From 4423e0a1dc9f12e852de6a2d4f942d255edfb028 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 19 Nov 2025 00:31:38 +0800 Subject: [PATCH 001/277] Fix mouse hover issue on menu bar when controlling Mac from iPad Fixes #8789 The issue was that when using the virtual/floating mouse on mobile devices to control a Mac, mouse clicks would occur at the wrong position, particularly noticeable in the menu bar where hover menus would disappear immediately. Root cause: The tapDown() method was sending mouse button events without ensuring the cursor position was updated on the server side first. This caused clicks to register at stale cursor positions. Solution: Before sending a mouse down event, check if the pointer has moved since entering the session. If not, send a mouse move event first to update the cursor position, then wait briefly before sending the click event. This ensures the remote cursor is at the correct position before any mouse button action is performed. --- flutter/lib/models/input_model.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 29d0cc0fd..8a3e60779 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -810,6 +810,10 @@ class InputModel { } Future tapDown(MouseButtons button) async { + if (!_pointerMovedAfterEnter) { + refreshMousePos(); + await Future.delayed(Duration(milliseconds: 10)); + } await sendMouse('down', button); } From 6f8af9d1145b700ef7b931d2b90002e8dbd2ab14 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 19 Nov 2025 11:03:06 +0800 Subject: [PATCH 002/277] refact: flatpak, socket x11, better compatibility (#13551) Signed-off-by: fufesou --- flatpak/rustdesk.json | 1 - 1 file changed, 1 deletion(-) diff --git a/flatpak/rustdesk.json b/flatpak/rustdesk.json index a99141f17..c4935e137 100644 --- a/flatpak/rustdesk.json +++ b/flatpak/rustdesk.json @@ -56,7 +56,6 @@ "finish-args": [ "--share=ipc", "--socket=x11", - "--socket=wayland", "--share=network", "--filesystem=home", "--device=dri", From 7d06de00fb29fcc2cfc93a722a1fe506923b1f74 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 19 Nov 2025 11:38:16 +0800 Subject: [PATCH 003/277] 24.04 ubuntu24.04 ubuntu24.04 ubuntu24.04 ubuntu24.04 ubuntu24.04 ubuntu24.04 ubuntu24.04 ubuntu24.04 ubuntu24.04 ubuntu24.04 ubuntu24.04 ubuntu24.04 ubuntu24.04 ubuntu24.04 ubuntu24.04 ubuntu24.04 ubuntu24.04 ubuntu24.04 ubuntu24.04 ubuntu --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6d0264a90..157bac491 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,7 +81,7 @@ jobs: # - { target: x86_64-apple-darwin , os: macos-10.15 } # - { target: x86_64-pc-windows-gnu , os: windows-2022 } # - { target: x86_64-pc-windows-msvc , os: windows-2022 } - - { target: x86_64-unknown-linux-gnu , os: ubuntu-22.04 } + - { target: x86_64-unknown-linux-gnu , os: ubuntu-24.04 } # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } steps: - name: Export GitHub Actions cache environment variables From 3787b45b49b405fd9e4d6e044eaa6de9474e5baa Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 20 Nov 2025 22:15:42 +0800 Subject: [PATCH 004/277] fix python scripts read offset (#13574) Signed-off-by: 21pages --- res/ab.py | 18 +++++++++--------- res/{device_group.py => device-groups.py} | 12 ++++++------ res/devices.py | 6 +++--- res/{user_group.py => user-groups.py} | 12 ++++++------ res/users.py | 6 +++--- 5 files changed, 27 insertions(+), 27 deletions(-) rename res/{device_group.py => device-groups.py} (97%) rename res/{user_group.py => user-groups.py} (97%) diff --git a/res/ab.py b/res/ab.py index c2ba59d2b..11f52282b 100644 --- a/res/ab.py +++ b/res/ab.py @@ -34,9 +34,10 @@ def view_shared_abs(url, token, name=None): filtered_params["pageSize"] = pageSize abs = [] - current = 1 + current = 0 while True: + current += 1 filtered_params["current"] = current response = requests.get(f"{url}/api/ab/shared/profiles", headers=headers, params=filtered_params) if response.status_code != 200: @@ -52,8 +53,7 @@ def view_shared_abs(url, token, name=None): abs.extend(data) total = response_json.get("total", 0) - current += pageSize - if len(data) < pageSize or current > total: + if len(data) < pageSize or current * pageSize >= total: break return abs @@ -86,9 +86,10 @@ def view_ab_peers(url, token, ab_guid, peer_id=None, alias=None): filtered_params["pageSize"] = pageSize peers = [] - current = 1 + current = 0 while True: + current += 1 filtered_params["current"] = current response = requests.get(f"{url}/api/ab/peers", headers=headers, params=filtered_params) if response.status_code != 200: @@ -104,8 +105,7 @@ def view_ab_peers(url, token, ab_guid, peer_id=None, alias=None): peers.extend(data) total = response_json.get("total", 0) - current += pageSize - if len(data) < pageSize or current > total: + if len(data) < pageSize or current * pageSize >= total: break return peers @@ -403,9 +403,10 @@ def view_ab_rules(url, token, ab_guid): } rules = [] - current = 1 + current = 0 while True: + current += 1 params["current"] = current response = requests.get(f"{url}/api/ab/rules", headers=headers, params=params) if response.status_code != 200: @@ -421,8 +422,7 @@ def view_ab_rules(url, token, ab_guid): rules.extend(data) total = response_json.get("total", 0) - current += pageSize - if len(data) < pageSize or current > total: + if len(data) < pageSize or current * pageSize >= total: break # Convert numeric permissions to string format diff --git a/res/device_group.py b/res/device-groups.py similarity index 97% rename from res/device_group.py rename to res/device-groups.py index ec98de15b..dd861aefc 100755 --- a/res/device_group.py +++ b/res/device-groups.py @@ -42,8 +42,9 @@ def list_groups(url, token, name=None, page_size=50): params = {"pageSize": page_size} if name: params["name"] = name - data, current = [], 1 + data, current = [], 0 while True: + current += 1 params["current"] = current r = requests.get(f"{url}/api/device-groups", headers=headers, params=params) if r.status_code != 200: @@ -56,8 +57,7 @@ def list_groups(url, token, name=None, page_size=50): rows = res.get("data", []) data.extend(rows) total = res.get("total", 0) - current += page_size - if len(rows) < page_size or current > total: + if len(rows) < page_size or current * page_size >= total: break return data @@ -142,8 +142,9 @@ def view_devices(url, token, group_name=None, id=None, device_name=None, params["pageSize"] = page_size - data, current = [], 1 + data, current = [], 0 while True: + current += 1 params["current"] = current r = requests.get(f"{url}/api/devices", headers=headers, params=params) if r.status_code != 200: @@ -152,8 +153,7 @@ def view_devices(url, token, group_name=None, id=None, device_name=None, rows = res.get("data", []) data.extend(rows) total = res.get("total", 0) - current += page_size - if len(rows) < page_size or current > total: + if len(rows) < page_size or current * page_size >= total: break return data diff --git a/res/devices.py b/res/devices.py index ba11866e5..832f0509b 100755 --- a/res/devices.py +++ b/res/devices.py @@ -34,9 +34,10 @@ def view( devices = [] - current = 1 + current = 0 while True: + current += 1 params["current"] = current response = requests.get(f"{url}/api/devices", headers=headers, params=params) if response.status_code != 200: @@ -61,8 +62,7 @@ def view( devices.append(device) total = response_json.get("total", 0) - current += pageSize - if len(data) < pageSize or current > total: + if len(data) < pageSize or current * pageSize >= total: break return devices diff --git a/res/user_group.py b/res/user-groups.py similarity index 97% rename from res/user_group.py rename to res/user-groups.py index 909123e4e..5df16c3b6 100755 --- a/res/user_group.py +++ b/res/user-groups.py @@ -42,8 +42,9 @@ def list_groups(url, token, name=None, page_size=50): params = {"pageSize": page_size} if name: params["name"] = name - data, current = [], 1 + data, current = [], 0 while True: + current += 1 params["current"] = current r = requests.get(f"{url}/api/user-groups", headers=headers, params=params) if r.status_code != 200: @@ -56,8 +57,7 @@ def list_groups(url, token, name=None, page_size=50): rows = res.get("data", []) data.extend(rows) total = res.get("total", 0) - current += page_size - if len(rows) < page_size or current > total: + if len(rows) < page_size or current * page_size >= total: break return data @@ -142,8 +142,9 @@ def view_users(url, token, group_name=None, name=None, page_size=50): params["pageSize"] = page_size - data, current = [], 1 + data, current = [], 0 while True: + current += 1 params["current"] = current r = requests.get(f"{url}/api/users", headers=headers, params=params) if r.status_code != 200: @@ -152,8 +153,7 @@ def view_users(url, token, group_name=None, name=None, page_size=50): rows = res.get("data", []) data.extend(rows) total = res.get("total", 0) - current += page_size - if len(rows) < page_size or current > total: + if len(rows) < page_size or current * page_size >= total: break return data diff --git a/res/users.py b/res/users.py index 86e562afd..02b114715 100755 --- a/res/users.py +++ b/res/users.py @@ -49,9 +49,10 @@ def view( users = [] - current = 1 + current = 0 while True: + current += 1 params["current"] = current response = requests.get(f"{url}/api/users", headers=headers, params=params) if response.status_code != 200: @@ -67,8 +68,7 @@ def view( users.extend(data) total = response_json.get("total", 0) - current += pageSize - if len(data) < pageSize or current > total: + if len(data) < pageSize or current * pageSize >= total: break return users From 3c0be4e40efaca2aff5d7c4cd2299aabc8f73d6c Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Thu, 20 Nov 2025 23:18:00 +0800 Subject: [PATCH 005/277] Revert "feat: macos, update dmg (#13539)" (#13577) This reverts commit a6571e71e47fbb74262eb5a3596afc14d02a8d13. --- src/common.rs | 4 --- src/core_main.rs | 35 +++++------------------- src/platform/macos.rs | 62 +++++++++---------------------------------- 3 files changed, 19 insertions(+), 82 deletions(-) diff --git a/src/common.rs b/src/common.rs index 2dbc4c964..4ac3b6cd9 100644 --- a/src/common.rs +++ b/src/common.rs @@ -115,10 +115,6 @@ pub fn global_init() -> bool { crate::server::wayland::init(); } } - #[cfg(target_os = "macos")] - { - crate::platform::macos::try_remove_temp_update_dir(None); - } true } diff --git a/src/core_main.rs b/src/core_main.rs index ab301e3d4..ecef5a45a 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -300,35 +300,14 @@ pub fn core_main() -> Option> { { use crate::platform; if args[0] == "--update" { - if args.len() > 1 && args[1].ends_with(".dmg") { - // Version check is unnecessary unless downgrading to an older version - // that lacks "update dmg" support. This is a special case since we cannot - // detect the version before extracting the DMG, so we skip the check. - let dmg_path = &args[1]; - println!("Updating from DMG: {}", dmg_path); - match platform::update_from_dmg(dmg_path) { - Ok(_) => { - println!("Update process from DMG started successfully."); - // The new process will handle the rest. We can exit. - } - Err(err) => { - eprintln!("Failed to start update from DMG: {}", err); - } + let _text = match platform::update_me() { + Ok(_) => { + log::info!("{}", translate("Update successfully!".to_string())); } - } else { - println!("Starting update process..."); - log::info!("Starting update process..."); - let _text = match platform::update_me() { - Ok(_) => { - println!("{}", translate("Update successfully!".to_string())); - log::info!("Update successfully!"); - } - Err(err) => { - eprintln!("Update failed with error: {}", err); - log::error!("Update failed with error: {err}"); - } - }; - } + Err(err) => { + log::error!("Update failed with error: {err}"); + } + }; return None; } } diff --git a/src/platform/macos.rs b/src/platform/macos.rs index bc13260a5..4bf419952 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -38,8 +38,6 @@ static PRIVILEGES_SCRIPTS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/platform/privileges_scripts"); static mut LATEST_SEED: i32 = 0; -// Using a fixed temporary directory for updates is preferable to -// using one that includes the custom client name. const UPDATE_TEMP_DIR: &str = "/tmp/.rustdeskupdate"; extern "C" { @@ -716,14 +714,6 @@ pub fn quit_gui() { }; } -#[inline] -pub fn try_remove_temp_update_dir(dir: Option<&str>) { - let target_path = Path::new(dir.unwrap_or(UPDATE_TEMP_DIR)); - if target_path.exists() { - std::fs::remove_dir_all(target_path).ok(); - } -} - pub fn update_me() -> ResultType<()> { let is_installed_daemon = is_installed_daemon(false); let option_stop_service = "stop-service"; @@ -743,7 +733,6 @@ pub fn update_me() -> ResultType<()> { bail!("Unknown app directory of current exe file: {:?}", cmd); }; - let app_name = crate::get_app_name(); if is_installed_daemon && !is_service_stopped { let agent = format!("{}_server.plist", crate::get_full_name()); let agent_plist_file = format!("/Library/LaunchAgents/{}", agent); @@ -760,13 +749,12 @@ pub fn update_me() -> ResultType<()> { let update_body = format!( r#" do shell script " -pgrep -x '{app_name}' | grep -v {pid} | xargs kill -9 && rm -rf '/Applications/{app_name}.app' && ditto '{app_dir}' '/Applications/{app_name}.app' && chown -R {user}:staff '/Applications/{app_name}.app' && xattr -r -d com.apple.quarantine '/Applications/{app_name}.app' -" with prompt "{app_name} wants to update itself" with administrator privileges +pgrep -x 'RustDesk' | grep -v {} | xargs kill -9 && rm -rf /Applications/RustDesk.app && ditto '{}' /Applications/RustDesk.app && chown -R {}:staff /Applications/RustDesk.app && xattr -r -d com.apple.quarantine /Applications/RustDesk.app +" with prompt "RustDesk wants to update itself" with administrator privileges "#, - app_name = app_name, - pid = std::process::id(), - app_dir = app_dir, - user = get_active_username() + std::process::id(), + app_dir, + get_active_username() ); match Command::new("osascript") .arg("-e") @@ -784,7 +772,7 @@ pgrep -x '{app_name}' | grep -v {pid} | xargs kill -9 && rm -rf '/Applications/{ } std::process::Command::new("open") .arg("-n") - .arg(&format!("/Applications/{}.app", app_name)) + .arg(&format!("/Applications/{}.app", crate::get_app_name())) .spawn() .ok(); // leave open a little time @@ -792,15 +780,6 @@ pgrep -x '{app_name}' | grep -v {pid} | xargs kill -9 && rm -rf '/Applications/{ Ok(()) } -pub fn update_from_dmg(dmg_path: &str) -> ResultType<()> { - println!("Starting update from DMG: {}", dmg_path); - extract_dmg(dmg_path, UPDATE_TEMP_DIR)?; - println!("DMG extracted"); - update_extracted(UPDATE_TEMP_DIR)?; - println!("Update process started"); - Ok(()) -} - pub fn update_to(_file: &str) -> ResultType<()> { update_extracted(UPDATE_TEMP_DIR)?; Ok(()) @@ -832,14 +811,10 @@ fn extract_dmg(dmg_path: &str, target_dir: &str) -> ResultType<()> { } std::fs::create_dir_all(target_path)?; - let status = Command::new("hdiutil") + Command::new("hdiutil") .args(&["attach", "-nobrowse", "-mountpoint", mount_point, dmg_path]) .status()?; - if !status.success() { - bail!("Failed to attach DMG image at {}: {:?}", dmg_path, status); - } - struct DmgGuard(&'static str); impl Drop for DmgGuard { fn drop(&mut self) { @@ -850,7 +825,7 @@ fn extract_dmg(dmg_path: &str, target_dir: &str) -> ResultType<()> { } let _guard = DmgGuard(mount_point); - let app_name = format!("{}.app", crate::get_app_name()); + let app_name = "RustDesk.app"; let src_path = format!("{}/{}", mount_point, app_name); let dest_path = format!("{}/{}", target_dir, app_name); @@ -859,12 +834,7 @@ fn extract_dmg(dmg_path: &str, target_dir: &str) -> ResultType<()> { .status()?; if !copy_status.success() { - bail!( - "Failed to copy application from {} to {}: {:?}", - src_path, - dest_path, - copy_status - ); + bail!("Failed to copy application {:?}", copy_status); } if !Path::new(&dest_path).exists() { @@ -878,13 +848,9 @@ fn extract_dmg(dmg_path: &str, target_dir: &str) -> ResultType<()> { } fn update_extracted(target_dir: &str) -> ResultType<()> { - let app_name = crate::get_app_name(); - let exe_path = format!( - "{}/{}.app/Contents/MacOS/{}", - target_dir, app_name, app_name - ); + let exe_path = format!("{}/RustDesk.app/Contents/MacOS/RustDesk", target_dir); let _child = unsafe { - if let Err(e) = Command::new(&exe_path) + Command::new(&exe_path) .arg("--update") .stdin(Stdio::null()) .stdout(Stdio::null()) @@ -893,11 +859,7 @@ fn update_extracted(target_dir: &str) -> ResultType<()> { hbb_common::libc::setsid(); Ok(()) }) - .spawn() - { - try_remove_temp_update_dir(Some(target_dir)); - bail!(e); - } + .spawn()? }; Ok(()) } From 426a68775f7fd222522b4703b513e3be62c41c6c Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 21 Nov 2025 10:27:37 +0800 Subject: [PATCH 006/277] feat: macos, update dmg (#13579) --- src/core_main.rs | 40 +++++++++++++++++++++++----- src/platform/macos.rs | 62 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 83 insertions(+), 19 deletions(-) diff --git a/src/core_main.rs b/src/core_main.rs index ecef5a45a..9abfcb444 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -181,6 +181,11 @@ pub fn core_main() -> Option> { #[cfg(not(any(target_os = "android", target_os = "ios")))] init_plugins(&args); if args.is_empty() || crate::common::is_empty_uni_link(&args[0]) { + #[cfg(target_os = "macos")] + { + crate::platform::macos::try_remove_temp_update_dir(None); + } + #[cfg(windows)] hbb_common::config::PeerConfig::preload_peers(); std::thread::spawn(move || crate::start_server(false, no_server)); @@ -300,14 +305,35 @@ pub fn core_main() -> Option> { { use crate::platform; if args[0] == "--update" { - let _text = match platform::update_me() { - Ok(_) => { - log::info!("{}", translate("Update successfully!".to_string())); + if args.len() > 1 && args[1].ends_with(".dmg") { + // Version check is unnecessary unless downgrading to an older version + // that lacks "update dmg" support. This is a special case since we cannot + // detect the version before extracting the DMG, so we skip the check. + let dmg_path = &args[1]; + println!("Updating from DMG: {}", dmg_path); + match platform::update_from_dmg(dmg_path) { + Ok(_) => { + println!("Update process from DMG started successfully."); + // The new process will handle the rest. We can exit. + } + Err(err) => { + eprintln!("Failed to start update from DMG: {}", err); + } } - Err(err) => { - log::error!("Update failed with error: {err}"); - } - }; + } else { + println!("Starting update process..."); + log::info!("Starting update process..."); + let _text = match platform::update_me() { + Ok(_) => { + println!("{}", translate("Update successfully!".to_string())); + log::info!("Update successfully!"); + } + Err(err) => { + eprintln!("Update failed with error: {}", err); + log::error!("Update failed with error: {err}"); + } + }; + } return None; } } diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 4bf419952..bc13260a5 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -38,6 +38,8 @@ static PRIVILEGES_SCRIPTS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/platform/privileges_scripts"); static mut LATEST_SEED: i32 = 0; +// Using a fixed temporary directory for updates is preferable to +// using one that includes the custom client name. const UPDATE_TEMP_DIR: &str = "/tmp/.rustdeskupdate"; extern "C" { @@ -714,6 +716,14 @@ pub fn quit_gui() { }; } +#[inline] +pub fn try_remove_temp_update_dir(dir: Option<&str>) { + let target_path = Path::new(dir.unwrap_or(UPDATE_TEMP_DIR)); + if target_path.exists() { + std::fs::remove_dir_all(target_path).ok(); + } +} + pub fn update_me() -> ResultType<()> { let is_installed_daemon = is_installed_daemon(false); let option_stop_service = "stop-service"; @@ -733,6 +743,7 @@ pub fn update_me() -> ResultType<()> { bail!("Unknown app directory of current exe file: {:?}", cmd); }; + let app_name = crate::get_app_name(); if is_installed_daemon && !is_service_stopped { let agent = format!("{}_server.plist", crate::get_full_name()); let agent_plist_file = format!("/Library/LaunchAgents/{}", agent); @@ -749,12 +760,13 @@ pub fn update_me() -> ResultType<()> { let update_body = format!( r#" do shell script " -pgrep -x 'RustDesk' | grep -v {} | xargs kill -9 && rm -rf /Applications/RustDesk.app && ditto '{}' /Applications/RustDesk.app && chown -R {}:staff /Applications/RustDesk.app && xattr -r -d com.apple.quarantine /Applications/RustDesk.app -" with prompt "RustDesk wants to update itself" with administrator privileges +pgrep -x '{app_name}' | grep -v {pid} | xargs kill -9 && rm -rf '/Applications/{app_name}.app' && ditto '{app_dir}' '/Applications/{app_name}.app' && chown -R {user}:staff '/Applications/{app_name}.app' && xattr -r -d com.apple.quarantine '/Applications/{app_name}.app' +" with prompt "{app_name} wants to update itself" with administrator privileges "#, - std::process::id(), - app_dir, - get_active_username() + app_name = app_name, + pid = std::process::id(), + app_dir = app_dir, + user = get_active_username() ); match Command::new("osascript") .arg("-e") @@ -772,7 +784,7 @@ pgrep -x 'RustDesk' | grep -v {} | xargs kill -9 && rm -rf /Applications/RustDes } std::process::Command::new("open") .arg("-n") - .arg(&format!("/Applications/{}.app", crate::get_app_name())) + .arg(&format!("/Applications/{}.app", app_name)) .spawn() .ok(); // leave open a little time @@ -780,6 +792,15 @@ pgrep -x 'RustDesk' | grep -v {} | xargs kill -9 && rm -rf /Applications/RustDes Ok(()) } +pub fn update_from_dmg(dmg_path: &str) -> ResultType<()> { + println!("Starting update from DMG: {}", dmg_path); + extract_dmg(dmg_path, UPDATE_TEMP_DIR)?; + println!("DMG extracted"); + update_extracted(UPDATE_TEMP_DIR)?; + println!("Update process started"); + Ok(()) +} + pub fn update_to(_file: &str) -> ResultType<()> { update_extracted(UPDATE_TEMP_DIR)?; Ok(()) @@ -811,10 +832,14 @@ fn extract_dmg(dmg_path: &str, target_dir: &str) -> ResultType<()> { } std::fs::create_dir_all(target_path)?; - Command::new("hdiutil") + let status = Command::new("hdiutil") .args(&["attach", "-nobrowse", "-mountpoint", mount_point, dmg_path]) .status()?; + if !status.success() { + bail!("Failed to attach DMG image at {}: {:?}", dmg_path, status); + } + struct DmgGuard(&'static str); impl Drop for DmgGuard { fn drop(&mut self) { @@ -825,7 +850,7 @@ fn extract_dmg(dmg_path: &str, target_dir: &str) -> ResultType<()> { } let _guard = DmgGuard(mount_point); - let app_name = "RustDesk.app"; + let app_name = format!("{}.app", crate::get_app_name()); let src_path = format!("{}/{}", mount_point, app_name); let dest_path = format!("{}/{}", target_dir, app_name); @@ -834,7 +859,12 @@ fn extract_dmg(dmg_path: &str, target_dir: &str) -> ResultType<()> { .status()?; if !copy_status.success() { - bail!("Failed to copy application {:?}", copy_status); + bail!( + "Failed to copy application from {} to {}: {:?}", + src_path, + dest_path, + copy_status + ); } if !Path::new(&dest_path).exists() { @@ -848,9 +878,13 @@ fn extract_dmg(dmg_path: &str, target_dir: &str) -> ResultType<()> { } fn update_extracted(target_dir: &str) -> ResultType<()> { - let exe_path = format!("{}/RustDesk.app/Contents/MacOS/RustDesk", target_dir); + let app_name = crate::get_app_name(); + let exe_path = format!( + "{}/{}.app/Contents/MacOS/{}", + target_dir, app_name, app_name + ); let _child = unsafe { - Command::new(&exe_path) + if let Err(e) = Command::new(&exe_path) .arg("--update") .stdin(Stdio::null()) .stdout(Stdio::null()) @@ -859,7 +893,11 @@ fn update_extracted(target_dir: &str) -> ResultType<()> { hbb_common::libc::setsid(); Ok(()) }) - .spawn()? + .spawn() + { + try_remove_temp_update_dir(Some(target_dir)); + bail!(e); + } }; Ok(()) } From 22b1dcaf7b1762cccd681604466e7f37de9768a8 Mon Sep 17 00:00:00 2001 From: summoner Date: Sat, 22 Nov 2025 08:16:02 +0100 Subject: [PATCH 007/277] Translation: Update hungarian hu.rs (#13578) * Translation: Update hungarian hu.rs Translate new strings Fix translation * Translation: update hu.rs Fix translation * Update hu.rs Fix translation --- src/lang/hu.rs | 88 +++++++++++++++++++++++++------------------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 423d176f9..8ee281470 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -28,7 +28,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable file transfer", "Fájlátvitel engedélyezése"), ("Enable TCP tunneling", "TCP-alagút engedélyezése"), ("IP Whitelisting", "IP engedélyezési lista"), - ("ID/Relay Server", "ID/Továbbító-kiszolgáló"), + ("ID/Relay Server", "Azonosító-/Továbbító-kiszolgáló"), ("Import server config", "Kiszolgáló-konfiguráció importálása"), ("Export Server Config", "Kiszolgáló-konfiguráció exportálása"), ("Import server configuration successfully", "Kiszolgáló-konfiguráció sikeresen importálva"), @@ -37,7 +37,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "A vágólap üres"), ("Stop service", "Szolgáltatás leállítása"), ("Change ID", "Azonosító módosítása"), - ("Your new ID", "Az új azonosító"), + ("Your new ID", "Új azonosító"), ("length %min% to %max%", "hossz %min% és %max% között"), ("starts with a letter", "betűvel kezdődik"), ("allowed characters", "engedélyezett karakterek"), @@ -50,11 +50,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Build Date", "Összeállítás ideje"), ("Version", "Verzió"), ("Home", "Kezdőképernyő"), - ("Audio Input", "Hangátvitel"), + ("Audio Input", "Hangbemenet"), ("Enhancements", "Fejlesztések"), ("Hardware Codec", "Hardveres kodek"), ("Adaptive bitrate", "Adaptív bitráta"), - ("ID Server", "ID-kiszolgáló"), + ("ID Server", "Azonosító-kiszolgáló"), ("Relay Server", "Továbbító-kiszolgáló"), ("API Server", "API-kiszolgáló"), ("invalid_http", "A címnek mindenképpen http(s)://-el kell kezdődnie."), @@ -127,7 +127,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Optimize reaction time", "Gyorsan reagáló"), ("Custom", "Egyéni"), ("Show remote cursor", "Távoli kurzor megjelenítése"), - ("Show quality monitor", "Kijelző minőségének ellenőrzése"), + ("Show quality monitor", "Kapcsolat minőségének megjelenítése"), ("Disable clipboard", "Közös vágólap kikapcsolása"), ("Lock after session end", "Távoli fiók zárolása a munkamenet végén"), ("Insert Ctrl + Alt + Del", "Illessze be a Ctrl + Alt + Del billentyűzetkombinációt"), @@ -148,8 +148,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("install_tip", "Előfordul, hogy bizonyos esetekben hiba léphet fel a Portable verzió használatakor. A megfelelő működés érdekében, telepítse a RustDesk alkalmazást a számítógépére."), ("Click to upgrade", "Kattintson ide a frissítés telepítéséhez"), ("Configure", "Beállítás"), - ("config_acc", "A számítógép távoli vezérléséhez a RustDesknek hozzáférési jogokat kell biztosítania."), - ("config_screen", "Ahhoz, hogy távolról hozzáférhessen számítógépéhez, meg kell adnia a RustDesknek a „Képernyőfelvétel” jogosultságot."), + ("config_acc", "A számítógép távoli vezérléséhez a RustDesknek hozzáférési jogokat kell adnia."), + ("config_screen", "Ahhoz, hogy távolról hozzáférhessen a számítógépéhez, meg kell adnia a RustDesknek a „Képernyőfelvétel” jogosultságot."), ("Installing ...", "Telepítés…"), ("Install", "Telepítés"), ("Installation", "Telepítés"), @@ -159,7 +159,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("agreement_tip", "A telepítés folytatásával automatikusan elfogadásra kerül a licenc szerződés."), ("Accept and Install", "Elfogadás és telepítés"), ("End-user license agreement", "Végfelhasználói licenc szerződés"), - ("Generating ...", "Létrehozás…"), + ("Generating ...", "Előállítás…"), ("Your installation is lower version.", "A telepített verzió alacsonyabb."), ("not_close_tcp_tip", "Ne zárja be ezt az ablakot, amíg TCP-alagutat használ"), ("Listening ...", "Figyelés…"), @@ -177,7 +177,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept", "Elfogadás"), ("Dismiss", "Elutasítás"), ("Disconnect", "Kapcsolat bontása"), - ("Enable file copy and paste", "Fájlok másolásának és beillesztésének engedélyezése"), + ("Enable file copy and paste", "Fájlmásolás és -beillesztés engedélyezése"), ("Connected", "Kapcsolódva"), ("Direct and encrypted connection", "Közvetlen, és titkosított kapcsolat"), ("Relayed and encrypted connection", "Továbbított, és titkosított kapcsolat"), @@ -220,7 +220,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logout", "Kilépés"), ("Tags", "Címkék"), ("Search ID", "Azonosító keresése…"), - ("whitelist_sep", "A címeket veszővel, pontosvesszővel, szóközzel, vagy új sorral válassza el"), + ("whitelist_sep", "A címeket vesszővel, pontosvesszővel, szóközzel vagy új sorral kell elválasztani"), ("Add ID", "Azonosító hozzáadása"), ("Add Tag", "Címke hozzáadása"), ("Unselect all tags", "A címkék kijelölésének megszüntetése"), @@ -239,7 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Socks5 Proxy", "Socks5 Proxy"), ("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"), ("Discovered", "Felfedezett"), - ("install_daemon_tip", "Az automatikus indításhoz szükséges a szolgáltatás telepítése"), + ("install_daemon_tip", "Automatikus indításhoz szükséges a szolgáltatás telepítése"), ("Remote ID", "Távoli azonosító"), ("Paste", "Beillesztés"), ("Paste here?", "Beillesztés ide?"), @@ -258,10 +258,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Three-Finger vertically", "Három ujj függőlegesen"), ("Mouse Wheel", "Egérgörgő"), ("Two-Finger Move", "Kétujjas mozgatás"), - ("Canvas Move", "Nézet mozgatása"), + ("Canvas Move", "Vászon mozgatása"), ("Pinch to Zoom", "Kétujjas nagyítás"), - ("Canvas Zoom", "Nézet nagyítása"), - ("Reset canvas", "Nézet visszaállítása"), + ("Canvas Zoom", "Vászon nagyítása"), + ("Reset canvas", "Vászon visszaállítása"), ("No permission of file transfer", "Nincs engedély a fájlátvitelre"), ("Note", "Megjegyzés"), ("Connection", "Kapcsolat"), @@ -374,7 +374,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Disconnected", "Kapcsolat bontva"), ("Other", "Egyéb"), ("Confirm before closing multiple tabs", "Biztosan bezárja az összes lapot?"), - ("Keyboard Settings", "Billentyűzet beállítások"), + ("Keyboard Settings", "Billentyűzet-beállítások"), ("Full Access", "Teljes hozzáférés"), ("Screen Share", "Képernyőmegosztás"), ("Wayland requires Ubuntu 21.04 or higher version.", "A Waylandhez Ubuntu 21.04 vagy újabb verzió szükséges."), @@ -448,13 +448,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resolution", "Felbontás"), ("No transfers in progress", "Nincs folyamatban átvitel"), ("Set one-time password length", "Állítsa be az egyszeri jelszó hosszát"), - ("RDP Settings", "RDP beállítások"), + ("RDP Settings", "RDP-beállítások"), ("Sort by", "Rendezés"), ("New Connection", "Új kapcsolat"), ("Restore", "Visszaállítás"), ("Minimize", "Minimalizálás"), ("Maximize", "Maximalizálás"), - ("Your Device", "Az Ön eszköze"), + ("Your Device", "Saját eszköz"), ("empty_recent_tip", "Nincsenek aktuális munkamenetek!\nIdeje ütemezni egy újat."), ("empty_favorite_tip", "Még nincs kedvenc távoli állomása?\nHagyja, hogy találjunk valakit, akivel kapcsolatba tud lépni, és adja hozzá a kedvencekhez!"), ("empty_lan_tip", "Úgy tűnik, még nem adott hozzá egyetlen távoli helyszínt sem."), @@ -469,7 +469,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("verify_rustdesk_password_tip", "RustDesk jelszó megerősítése"), ("remember_account_tip", "Emlékezzen erre a fiókra"), ("os_account_desk_tip", "Ezzel a fiókkal bejelentkezhet a távoli operációs rendszerbe, és aktiválhatja az asztali munkamenetet fej nélküli módban."), - ("OS Account", "OS fiók"), + ("OS Account", "OS-fiók"), ("another_user_login_title_tip", "Egy másik felhasználó már bejelentkezett."), ("another_user_login_text_tip", "Különálló"), ("xorg_not_found_title_tip", "Xorg nem található."), @@ -515,7 +515,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Already exists", "Már létezik"), ("Change Password", "Jelszó módosítása"), ("Refresh Password", "Jelszó frissítése"), - ("ID", "ID"), + ("ID", "Azonosító"), ("Grid View", "Mozaik nézet"), ("List View", "Lista nézet"), ("Select", "Kiválasztás"), @@ -543,13 +543,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("upgrade_rustdesk_server_pro_to_{}_tip", "Frissítse a RustDesk Server Prot a(z) {} vagy újabb verzióra!"), ("pull_group_failed_tip", "A csoport frissítése nem sikerült"), ("Filter by intersection", "Szűrés metszéspontok szerint"), - ("Remove wallpaper during incoming sessions", "Távolítsa el a háttérképet a bejövő munkamenetek közben"), + ("Remove wallpaper during incoming sessions", "Háttérkép eltávolítása bejövő munkameneteknél"), ("Test", "Teszt"), ("display_is_plugged_out_msg", "A képernyő nincs csatlakoztatva, váltson az első képernyőre."), ("No displays", "Nincsenek kijelzők"), ("Open in new window", "Megnyitás új ablakban"), ("Show displays as individual windows", "Kijelzők megjelenítése egyedi ablakokként"), - ("Use all my displays for the remote session", "Az összes kijelzőm használata a távoli munkamenethez"), + ("Use all my displays for the remote session", "Összes kijelző használata a távoli munkamenethez"), ("selinux_tip", "A SELinux engedélyezve van az eszközén, ami azt okozhatja, hogy a RustDesk nem fut megfelelően, mint ellenőrzött."), ("Change view", "Nézet módosítása"), ("Big tiles", "Nagy csempék"), @@ -569,7 +569,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input_source_2_tip", "2. bemeneti forrás"), ("Swap control-command key", "Vezérlő- és parancsgombok cseréje"), ("swap-left-right-mouse", "Bal és jobb egérgomb felcserélése"), - ("2FA code", "2FA kód"), + ("2FA code", "2FA-kód"), ("More", "Továbbiak"), ("enable-2fa-title", "Kétfaktoros hitelesítés aktiválása"), ("enable-2fa-desc", "Állítsa be a hitelesítőt. Használhat egy hitelesítő alkalmazást, például az Aegis, Authy, a Microsoft- vagy a Google Authenticator alkalmazást a telefonján vagy az asztali számítógépén.\n\nOlvassa be a QR-kódot az alkalmazással, és adja meg az alkalmazás által megjelenített kódot a kétfaktoros hitelesítés aktiválásához."), @@ -604,7 +604,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Outgoing", "Kimenő"), ("Clear Wayland screen selection", "Wayland képernyő kiválasztásának törlése"), ("clear_Wayland_screen_selection_tip", "A képernyőválasztás törlése után újra kiválaszthatja a megosztandó képernyőt."), - ("confirm_clear_Wayland_screen_selection_tip", "Biztos, hogy törölni szeretné a Wayland képernyő kiválasztását?"), + ("confirm_clear_Wayland_screen_selection_tip", "Biztosan törölni szeretné a Wayland képernyő kiválasztását?"), ("android_new_voice_call_tip", "Új hanghívás-kérés érkezett. Ha elfogadja a megkeresést, a hang átvált hangkommunikációra."), ("texture_render_tip", "Használja a textúra leképezést a képek simábbá tételéhez. Ezt az opciót kikapcsolhatja, ha leképezési problémái vannak."), ("Use texture rendering", "Textúra leképezés használata"), @@ -619,11 +619,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Apps", "Alkalmazások"), ("Volume up", "Hangerő fel"), ("Volume down", "Hangerő le"), - ("Power", "Teljesítmény"), + ("Power", "Főkapcsoló"), ("Telegram bot", "Telegram bot"), ("enable-bot-tip", "Ha aktiválja ezt a funkciót, akkor a 2FA-kódot a botjától kaphatja meg. Kapcsolati értesítésként is használható."), ("enable-bot-desc", "1. Nyisson csevegést @BotFather.\n2. Küldje el a „/newbot” parancsot. Miután ezt a lépést elvégezte, kap egy tokent.\n3. Indítson csevegést az újonnan létrehozott botjával. Küldjön egy olyan üzenetet, amely egy perjel („/”) kezdetű, pl. „/hello” az aktiváláshoz.\n"), - ("cancel-2fa-confirm-tip", "Biztosan le akarja mondani a 2FA-t?"), + ("cancel-2fa-confirm-tip", "Biztosan vissza akarja vonni a 2FA-hitelesítést?"), ("cancel-bot-confirm-tip", "Biztosan le akarja mondani a Telegram botot?"), ("About RustDesk", "A RustDesk névjegye"), ("Send clipboard keystrokes", "Billentyűleütések küldése a vágólapra"), @@ -648,13 +648,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Mappa feltöltése"), ("Upload files", "Fájlok feltöltése"), ("Clipboard is synchronized", "A vágólap szinkronizálva van"), - ("Update client clipboard", "Az ügyfél vágólapjának frissítése"), + ("Update client clipboard", "Kliens vágólapjának frissítése"), ("Untagged", "Címkézetlen"), ("new-version-of-{}-tip", "A(z) {} új verziója"), ("Accessible devices", "Hozzáférhető eszközök"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Frissítse a RustDesk klienst {} vagy újabb verziójára a távoli oldalon!"), - ("d3d_render_tip", "D3D leképezés"), - ("Use D3D rendering", "D3D leképezés használata"), + ("d3d_render_tip", "D3D-leképezés"), + ("Use D3D rendering", "D3D-leképezés használata"), ("Printer", "Nyomtató"), ("printer-os-requirement-tip", "Nyomtató operációs rendszerének minimális rendszerkövetelménye"), ("printer-requires-installed-{}-client-tip", "A nyomtatóhoz szükséges a(z) {} kliens telepítése"), @@ -679,12 +679,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Save as", "Mentés másként"), ("Copy to clipboard", "Másolás a vágólapra"), ("Enable remote printer", "Távoli nyomtatók engedélyezése"), - ("Downloading {}", "Letöltés {}"), - ("{} Update", "{} Frissítés"), - ("{}-to-update-tip", "A {} bezárása és az új verzió telepítése."), + ("Downloading {}", "{} letöltése"), + ("{} Update", "{} frissítés"), + ("{}-to-update-tip", "A(z) {} bezárása és az új verzió telepítése."), ("download-new-version-failed-tip", "Ha a letöltés sikertelen, akkor vagy újrapróbálkozhat, vagy a „Letöltés” gombra kattintva letöltheti a kiadási oldalról, és manuálisan frissíthet."), ("Auto update", "Automatikus frissítés"), - ("update-failed-check-msi-tip", "A telepítési módszer felismerése nem sikerült. Kérjük, kattintson a „Letöltés” gombra, hogy letöltse a kiadási oldalról, és manuálisan frissítse."), + ("update-failed-check-msi-tip", "A telepítési módszer felismerése nem sikerült. Kattintson a „Letöltés” gombra, hogy letöltse a kiadási oldalról, és manuálisan frissítse."), ("websocket_tip", "WebSocket használatakor csak a relé-kapcsolatok támogatottak."), ("Use WebSocket", "WebSocket használata"), ("Trackpad speed", "Érintőpad sebessége"), @@ -701,14 +701,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("New tab", "Új lap"), ("Keep terminal sessions on disconnect", "Terminál munkamenetek megtartása leválasztáskor"), ("Terminal (Run as administrator)", "Terminál (rendszergazdaként futtatva)"), - ("terminal-admin-login-tip", "Kérjük, adja meg a felügyelt terminál rendszergazdai fiókjának jelszavát."), + ("terminal-admin-login-tip", "Adja meg a felügyelt terminál rendszergazdai fiókjának jelszavát."), ("Failed to get user token.", "Hiba a felhasználói token lekérdezésekor."), ("Incorrect username or password.", "A felhasználónév vagy a jelszó helytelen."), ("The user is not an administrator.", "A felhasználó nem rendszergazda."), ("Failed to check if the user is an administrator.", "Hiba merült fel annak ellenőrzése során, hogy a felhasználó rendszergazda-e."), ("Supported only in the installed version.", "Csak a telepített változatban támogatott."), - ("elevation_username_tip", "Felhasználónév vagy tartománynév megadása\\felhasználónév"), - ("Preparing for installation ...", "Felkészülés a telepítésre ..."), + ("elevation_username_tip", "Felhasználónév vagy tartománynév megadása"), + ("Preparing for installation ...", "Felkészülés a telepítésre…"), ("Show my cursor", "Kurzor megjelenítése"), ("Scale custom", "Egyéni méretarány"), ("Custom scale slider", "Egyéni méretarány-csúszka"), @@ -719,15 +719,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Small", "Kicsi"), ("Large", "Nagy"), ("Show virtual joystick", "Virtuális vezérlő megjelenítése"), - ("Edit note", "Jegyzet szerkesztése"), + ("Edit note", "Megjegyzés szerkesztése"), ("Alias", "Álnév"), - ("ScrollEdge", ""), - ("Allow insecure TLS fallback", ""), - ("allow-insecure-tls-fallback-tip", ""), - ("Disable UDP", ""), - ("disable-udp-tip", ""), - ("server-oss-not-support-tip", ""), - ("input note here", ""), - ("note-at-conn-end-tip", ""), + ("ScrollEdge", "Görgetés az ablak szélein"), + ("Allow insecure TLS fallback", "Nem biztonságos TLS-tartalék engedélyezése"), + ("allow-insecure-tls-fallback-tip", "Alapértelmezés szerint a RustDesk ellenőrzi a kiszolgáló tanúsítványát a TLS-protokollok esetében. Ha ez a beállítás engedélyezve van, a RustDesk kihagyja az ellenőrzési lépést, és az ellenőrzés sikertelensége esetén folytatja a műveletet."), + ("Disable UDP", "UDP letiltása"), + ("disable-udp-tip", "Meghatározza, hogy csak TCP-t használjon-e. Ha ez az beállítás engedélyezve van, a RustDesk nem fogja többé használni a 21116-os UDP-portot, helyette a 21116-os TCP-portot fogja használni."), + ("server-oss-not-support-tip", "MEGJEGYZÉS: Az OSS RustDesk kiszolgáló nem támogatja ezt a funkciót."), + ("input note here", "Megjegyzés bevitele"), + ("note-at-conn-end-tip", "Megjegyzés a kapcsolat végén"), ].iter().cloned().collect(); } From 33e14939328aca6ca9884960c686ad9441ffa5b1 Mon Sep 17 00:00:00 2001 From: XLion Date: Tue, 25 Nov 2025 01:08:48 +0800 Subject: [PATCH 008/277] Update tw.rs; Add space for cn.rs (#13609) * Update tw.rs * Update cn.rs * Update tw.rs * Update tw.rs --- src/lang/cn.rs | 4 ++-- src/lang/tw.rs | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 0b9475e00..db03e2fbc 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -725,8 +725,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Allow insecure TLS fallback", "允许回退到不安全的 TLS 连接"), ("allow-insecure-tls-fallback-tip", "默认情况下,对于使用 TLS 的协议,RustDesk 会验证服务器证书。\n启用此选项后,在验证失败时,RustDesk 将转为跳过验证步骤并继续连接。"), ("Disable UDP", "禁用 UDP"), - ("disable-udp-tip", "控制是否仅使用TCP。\n启用此选项后,RustDesk 将不再使用UDP 21116,而是使用TCP 21116。"), - ("server-oss-not-support-tip", "注意:RustDesk 开源服务器(OSS server) 不包含此功能。"), + ("disable-udp-tip", "控制是否仅使用 TCP。\n启用此选项后,RustDesk 将不再使用 UDP 21116,而是使用 TCP 21116。"), + ("server-oss-not-support-tip", "注意:RustDesk 开源服务器 (OSS server) 不包含此功能。"), ("input note here", "输入备注"), ("note-at-conn-end-tip", "在连接结束时请求备注"), ].iter().cloned().collect(); diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 1bf7f3ebc..7a8f0ec06 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -721,13 +721,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", "顯示虛擬搖桿"), ("Edit note", "編輯備註"), ("Alias", "別名"), - ("ScrollEdge", ""), - ("Allow insecure TLS fallback", ""), - ("allow-insecure-tls-fallback-tip", ""), - ("Disable UDP", ""), - ("disable-udp-tip", ""), - ("server-oss-not-support-tip", ""), - ("input note here", ""), - ("note-at-conn-end-tip", ""), + ("ScrollEdge", "邊緣滾動"), + ("Allow insecure TLS fallback", "允許降級到不安全的 TLS 連接"), + ("allow-insecure-tls-fallback-tip", "預設情況下,對於使用 TLS 的協定,RustDesk 會驗證伺服器的憑證。\n啟用此選項後,在驗證失敗時,RustDesk 將轉為跳過驗證步驟並繼續連接。"), + ("Disable UDP", "停用 UDP"), + ("disable-udp-tip", "控制是否僅使用 TCP。\n啟用此選項後,RustDesk 將不再使用 UDP 21116,而是使用 TCP 21116。"), + ("server-oss-not-support-tip", "注意:RustDesk 開源伺服器 (OSS server) 不包含此功能。"), + ("input note here", "輸入備註"), + ("note-at-conn-end-tip", "在連接結束時請求備註"), ].iter().cloned().collect(); } From ae06f27372d8a906013a0f1af926f9c6ad7733d3 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 25 Nov 2025 23:05:31 +0800 Subject: [PATCH 009/277] fix: sciter, cursor position mismatch (#13629) Signed-off-by: fufesou --- src/ui/remote.tis | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ui/remote.tis b/src/ui/remote.tis index 8dd3a12fd..0dd574af7 100644 --- a/src/ui/remote.tis +++ b/src/ui/remote.tis @@ -90,8 +90,10 @@ function adaptDisplay() { } if (isRemoteLinux()) { cursor_scale = display_scale * display_remote_scale; - if (cursor_scale <= 0.0001) cursor_scale = 1.; + } else { + cursor_scale = display_scale; } + if (cursor_scale <= 0.0001) cursor_scale = 1.; refreshCursor(); handler.style.set { width: w / scaleFactor + "px", From 4ed8696d1d1731f24b00c424e632a283b3b75298 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 26 Nov 2025 19:15:32 +0800 Subject: [PATCH 010/277] fix: file transfer, jobs lost if conn is not established (#13635) Signed-off-by: fufesou --- src/client/io_loop.rs | 4 +++- src/ui_session_interface.rs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 3f30949bd..2b52c7233 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1054,7 +1054,9 @@ impl Remote { } pub async fn sync_jobs_status_to_local(&mut self) -> bool { - log::info!("sync transfer job status"); + if !self.is_connected { + return false; + } let mut config: PeerConfig = self.handler.load_config(); let mut transfer_metas = TransferSerde::default(); for job in self.read_jobs.iter() { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index c58fe8959..be1baa587 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -2009,7 +2009,7 @@ pub async fn io_loop(handler: Session, round: u32) { } let mut remote = Remote::new(handler, receiver, sender); remote.io_loop(&key, &token, round).await; - remote.sync_jobs_status_to_local().await; + let _ = remote.sync_jobs_status_to_local().await; } #[cfg(not(any(target_os = "android", target_os = "ios")))] From 5b214418982b106b3faf78295a139e99d772ffd8 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 28 Nov 2025 10:45:48 +0800 Subject: [PATCH 011/277] webrtc --- Cargo.lock | 1018 ++++++++++++++++++++++++++++++++++++++++++++--- build.rs | 2 +- libs/hbb_common | 2 +- 3 files changed, 966 insertions(+), 56 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b6b927eb0..33ba832d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,6 +33,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "aes" version = "0.8.4" @@ -44,6 +54,20 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.7.8" @@ -221,9 +245,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.7" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" @@ -273,13 +297,19 @@ dependencies = [ "objc2-foundation", "parking_lot", "percent-encoding", - "serde 1.0.203", + "serde 1.0.228", "serde_derive", "windows-sys 0.48.0", "wl-clipboard-rs", "x11rb 0.13.1", ] +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "arrayref" version = "0.3.9" @@ -298,6 +328,45 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" +[[package]] +name = "asn1-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits 0.2.19", + "rusticata-macros", + "thiserror 1.0.61", + "time 0.3.36", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + [[package]] name = "associative-cache" version = "1.0.1" @@ -573,6 +642,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base32" version = "0.4.0" @@ -597,6 +672,15 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde 1.0.228", +] + [[package]] name = "bindgen" version = "0.59.2" @@ -699,7 +783,7 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" dependencies = [ - "serde 1.0.203", + "serde 1.0.228", ] [[package]] @@ -739,6 +823,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + [[package]] name = "block-sys" version = "0.1.0-beta.1" @@ -855,7 +948,7 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" dependencies = [ - "serde 1.0.203", + "serde 1.0.228", ] [[package]] @@ -973,6 +1066,15 @@ dependencies = [ "wayland-client", ] +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.2.13" @@ -984,6 +1086,18 @@ dependencies = [ "shlex", ] +[[package]] +name = "ccm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae3c82e4355234767756212c570e29833699ab63e6ffd161887314cc5b43847" +dependencies = [ + "aead", + "cipher", + "ctr", + "subtle", +] + [[package]] name = "cesu8" version = "1.1.0" @@ -1033,6 +1147,30 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if 1.0.0", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "chrono" version = "0.4.41" @@ -1082,6 +1220,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", + "zeroize", ] [[package]] @@ -1112,18 +1251,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.8" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b3edb18336f4df585bc9aa31dd99c036dfa5dc5e9a2939a722a188f3a8970d" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.8" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1c09dd5ada6c6c78075d6fd0da3f90d8080651e2d6cc8eb2f1aaa4034ced708" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstream", "anstyle", @@ -1133,9 +1272,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.1" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "clipboard" @@ -1157,7 +1296,7 @@ dependencies = [ "parking_lot", "percent-encoding", "rand 0.8.5", - "serde 1.0.203", + "serde 1.0.228", "serde_derive", "thiserror 1.0.61", "utf16string", @@ -1326,7 +1465,7 @@ version = "0.4.0-2" source = "git+https://github.com/rustdesk-org/confy#83db9ec19a2f97e9718aef69e4fc5611bb382479" dependencies = [ "directories-next", - "serde 1.0.203", + "serde 1.0.228", "thiserror 1.0.61", "toml 0.5.11", ] @@ -1341,6 +1480,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "const_fn" version = "0.4.10" @@ -1682,6 +1827,18 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -1689,6 +1846,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -1698,6 +1856,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f791803201ab277ace03903de1594460708d2d54df6053f2d9e82f592b19e3b" +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "ctrlc" version = "3.4.4" @@ -1714,6 +1881,32 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + [[package]] name = "dart-sys" version = "4.1.5" @@ -1928,6 +2121,31 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits 0.2.19", + "rusticata-macros", +] + [[package]] name = "deranged" version = "0.3.11" @@ -1955,6 +2173,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -2047,6 +2266,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + [[package]] name = "dlib" version = "0.5.2" @@ -2116,7 +2346,7 @@ checksum = "7f3f119846c823f9eafcf953a8f6ffb6ed69bf6240883261a7f13b634579a51f" dependencies = [ "lazy_static", "regex", - "serde 1.0.203", + "serde 1.0.228", "strsim 0.10.0", ] @@ -2171,6 +2401,42 @@ dependencies = [ "linux-raw-sys 0.6.5", ] +[[package]] +name = "dtls" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f531dd7c181beaf3cebab3716afa4d0d41ab888be85232583f56bbaf07ca208a" +dependencies = [ + "aes", + "aes-gcm", + "async-trait", + "bincode", + "byteorder", + "cbc", + "ccm", + "chacha20poly1305", + "der-parser", + "hmac", + "log", + "p256", + "p384", + "portable-atomic", + "rand 0.9.2", + "rand_core 0.6.4", + "rcgen", + "ring", + "rustls", + "sec1", + "serde 1.0.228", + "sha1", + "sha2", + "thiserror 1.0.61", + "tokio", + "webrtc-util", + "x25519-dalek", + "x509-parser", +] + [[package]] name = "dtoa" version = "0.4.8" @@ -2190,18 +2456,32 @@ dependencies = [ "cc", "hbb_common", "lazy_static", - "serde 1.0.203", + "serde 1.0.228", "serde_derive", "thiserror 1.0.61", ] +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature 2.2.0", + "spki", +] + [[package]] name = "ed25519" version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" dependencies = [ - "signature", + "signature 1.6.4", ] [[package]] @@ -2210,6 +2490,27 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "enigo" version = "0.0.14" @@ -2220,7 +2521,7 @@ dependencies = [ "objc", "pkg-config", "rdev", - "serde 1.0.203", + "serde 1.0.228", "serde_derive", "tfc", "unicode-segmentation", @@ -2263,7 +2564,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" dependencies = [ "enumflags2_derive", - "serde 1.0.203", + "serde 1.0.228", ] [[package]] @@ -2443,6 +2744,22 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "field-offset" version = "0.3.6" @@ -2911,6 +3228,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -2960,6 +3278,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gif" version = "0.13.1" @@ -3141,6 +3469,17 @@ dependencies = [ "system-deps 6.2.2", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "gstreamer" version = "0.16.7" @@ -3367,6 +3706,7 @@ dependencies = [ "base64 0.22.1", "bytes", "chrono", + "clap 4.5.53", "confy", "default_net", "directories-next", @@ -3391,7 +3731,7 @@ dependencies = [ "rustls-native-certs", "rustls-pki-types", "rustls-platform-verifier", - "serde 1.0.203", + "serde 1.0.228", "serde_derive", "serde_json 1.0.118", "sha2", @@ -3411,6 +3751,7 @@ dependencies = [ "url", "uuid", "webpki-roots 1.0.4", + "webrtc", "whoami", "winapi 0.3.9", "zstd 0.13.1", @@ -3470,6 +3811,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -3557,7 +3907,7 @@ dependencies = [ "bindgen 0.59.2", "cc", "log", - "serde 1.0.203", + "serde 1.0.228", "serde_derive", "serde_json 1.0.118", ] @@ -3768,6 +4118,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" dependencies = [ + "block-padding", "generic-array", ] @@ -3780,6 +4131,27 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "interceptor" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea51375727680dc15f06e8ad90fa31df75d79dd030100e8ad60eef1c27fe2c98" +dependencies = [ + "async-trait", + "bytes", + "futures", + "log", + "portable-atomic", + "rand 0.9.2", + "rtcp", + "rtp", + "thiserror 1.0.61", + "tokio", + "waitgroup", + "webrtc-srtp", + "webrtc-util", +] + [[package]] name = "io-lifetimes" version = "1.0.11" @@ -3813,7 +4185,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" dependencies = [ "memchr", - "serde 1.0.203", + "serde 1.0.228", ] [[package]] @@ -3973,7 +4345,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ "bitflags 2.9.1", - "serde 1.0.203", + "serde 1.0.228", "unicode-segmentation", ] @@ -4298,6 +4670,16 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if 1.0.0", + "digest", +] + [[package]] name = "md5" version = "0.7.0" @@ -4665,6 +5047,7 @@ dependencies = [ "cfg-if 1.0.0", "libc", "memoffset 0.7.1", + "pin-utils", ] [[package]] @@ -5239,12 +5622,27 @@ dependencies = [ "cc", ] +[[package]] +name = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openssl" version = "0.10.68" @@ -5353,7 +5751,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae99c7fa6dd38c7cafe1ec085e804f8f555a2f8659b0dbe03f1f9963a9b51092" dependencies = [ "log", - "serde 1.0.203", + "serde 1.0.228", "windows-sys 0.52.0", ] @@ -5373,7 +5771,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38731fa859ef679f1aec66ca9562165926b442f298467f76f5990f431efe87dc" dependencies = [ - "serde 1.0.203", + "serde 1.0.228", "serde_derive", "serde_json 1.0.118", ] @@ -5393,6 +5791,30 @@ dependencies = [ "ttf-parser", ] +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "page_size" version = "0.6.0" @@ -5536,6 +5958,25 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -5696,6 +6137,16 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.30" @@ -5712,7 +6163,7 @@ dependencies = [ "indexmap", "line-wrap", "quick-xml 0.31.0", - "serde 1.0.203", + "serde 1.0.228", "time 0.3.36", ] @@ -5760,6 +6211,35 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + [[package]] name = "portable-pty" version = "0.8.1" @@ -5817,6 +6297,15 @@ dependencies = [ "num-integer", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro-crate" version = "0.1.5" @@ -6035,7 +6524,7 @@ dependencies = [ "bytes", "getrandom 0.3.2", "lru-slab", - "rand 0.9.0", + "rand 0.9.2", "ring", "rustc-hash 2.1.1", "rustls", @@ -6123,13 +6612,12 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.0" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", - "zerocopy 0.8.26", ] [[package]] @@ -6289,6 +6777,20 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time 0.3.36", + "x509-parser", + "yasna", +] + [[package]] name = "rdev" version = "0.5.0-2" @@ -6436,7 +6938,7 @@ dependencies = [ "rustls", "rustls-native-certs", "rustls-pki-types", - "serde 1.0.203", + "serde 1.0.228", "serde_json 1.0.118", "serde_urlencoded", "sync_wrapper", @@ -6454,6 +6956,16 @@ dependencies = [ "webpki-roots 1.0.4", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "rgb" version = "0.8.50" @@ -6514,6 +7026,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rtcp" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81d30d1c4091644431c22acf9f8be6191b56805e0e977f15ca7104b4a6d6eaec" +dependencies = [ + "bytes", + "thiserror 1.0.61", + "webrtc-util", +] + [[package]] name = "rtoolbox" version = "0.0.2" @@ -6524,6 +7047,21 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rtp" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f126f38ea84c02480e32e547c1459a939052f74fb92117ac3eef23fdac6b023" +dependencies = [ + "bytes", + "memchr", + "portable-atomic", + "rand 0.9.2", + "serde 1.0.228", + "thiserror 1.0.61", + "webrtc-util", +] + [[package]] name = "rubato" version = "0.12.0" @@ -6608,7 +7146,7 @@ dependencies = [ "cfg-if 1.0.0", "chrono", "cidr-utils", - "clap 4.5.8", + "clap 4.5.53", "clipboard", "clipboard-master", "cocoa 0.24.1", @@ -6673,7 +7211,7 @@ dependencies = [ "samplerate", "sciter-rs", "scrap", - "serde 1.0.203", + "serde 1.0.228", "serde_derive", "serde_json 1.0.118", "serde_repr", @@ -6737,6 +7275,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustix" version = "0.37.27" @@ -6938,7 +7485,7 @@ dependencies = [ "pkg-config", "quest", "repng", - "serde 1.0.203", + "serde 1.0.228", "serde_json 1.0.118", "target_build_utils", "tracing", @@ -6960,6 +7507,32 @@ dependencies = [ "tiny-skia", ] +[[package]] +name = "sdp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c374dceda16965d541c8800ce9cc4e1c14acfd661ddf7952feeedc3411e5c6" +dependencies = [ + "rand 0.9.2", + "substring", + "thiserror 1.0.61", + "url", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "2.10.0" @@ -7010,18 +7583,28 @@ checksum = "34b623917345a631dc9608d5194cc206b3fe6c3554cd1c75b937e55e285254af" [[package]] name = "serde" -version = "1.0.203" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.203" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2 1.0.93", "quote 1.0.36", @@ -7048,7 +7631,7 @@ checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4" dependencies = [ "itoa 1.0.11", "ryu", - "serde 1.0.203", + "serde 1.0.228", ] [[package]] @@ -7068,7 +7651,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" dependencies = [ - "serde 1.0.203", + "serde 1.0.228", ] [[package]] @@ -7080,7 +7663,7 @@ dependencies = [ "form_urlencoded", "itoa 1.0.11", "ryu", - "serde 1.0.203", + "serde 1.0.228", ] [[package]] @@ -7225,6 +7808,16 @@ version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -7325,7 +7918,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" dependencies = [ - "serde 1.0.203", + "serde 1.0.228", ] [[package]] @@ -7368,7 +7961,7 @@ dependencies = [ "ed25519", "libc", "libsodium-sys", - "serde 1.0.203", + "serde 1.0.228", ] [[package]] @@ -7413,6 +8006,16 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -7486,6 +8089,25 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "stun" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a512c5d501e3e3b5a4bb3e8e31462d56d54a66b95a28b8596e14422bf21c32b" +dependencies = [ + "base64 0.22.1", + "crc", + "lazy_static", + "md-5", + "rand 0.9.2", + "ring", + "subtle", + "thiserror 1.0.61", + "tokio", + "url", + "webrtc-util", +] + [[package]] name = "stun_codec" version = "0.3.5" @@ -7513,6 +8135,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "substring" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ee6433ecef213b2e72f587ef64a2f5943e7cd16fbd82dbe8bc07486c534c86" +dependencies = [ + "autocfg 1.3.0", +] + [[package]] name = "subtle" version = "2.6.1" @@ -7561,6 +8192,17 @@ dependencies = [ "futures-core", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + [[package]] name = "sys-locale" version = "0.3.1" @@ -7892,7 +8534,7 @@ dependencies = [ "num-conv", "num_threads", "powerfmt", - "serde 1.0.203", + "serde 1.0.228", "time-core", "time-macros", ] @@ -8075,7 +8717,7 @@ version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" dependencies = [ - "serde 1.0.203", + "serde 1.0.228", ] [[package]] @@ -8084,7 +8726,7 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" dependencies = [ - "serde 1.0.203", + "serde 1.0.228", "serde_spanned", "toml_datetime", "toml_edit 0.19.15", @@ -8096,7 +8738,7 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" dependencies = [ - "serde 1.0.203", + "serde 1.0.228", "serde_spanned", "toml_datetime", "toml_edit 0.20.2", @@ -8108,7 +8750,7 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" dependencies = [ - "serde 1.0.203", + "serde 1.0.228", ] [[package]] @@ -8118,7 +8760,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap", - "serde 1.0.203", + "serde 1.0.228", "serde_spanned", "toml_datetime", "winnow", @@ -8131,7 +8773,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ "indexmap", - "serde 1.0.203", + "serde 1.0.228", "serde_spanned", "toml_datetime", "winnow", @@ -8355,7 +8997,7 @@ dependencies = [ "httparse", "log", "native-tls", - "rand 0.9.0", + "rand 0.9.2", "rustls", "rustls-native-certs", "rustls-pki-types", @@ -8365,6 +9007,27 @@ dependencies = [ "webpki-roots 0.26.9", ] +[[package]] +name = "turn" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ed995882f66ab94238de77c62e5e778389698ab700afa4696f4754da8f457cb" +dependencies = [ + "async-trait", + "base64 0.22.1", + "futures", + "log", + "md-5", + "portable-atomic", + "rand 0.9.2", + "ring", + "stun", + "thiserror 1.0.61", + "tokio", + "tokio-util", + "webrtc-util", +] + [[package]] name = "typenum" version = "1.17.0" @@ -8482,6 +9145,12 @@ dependencies = [ "unic-common", ] +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + [[package]] name = "unicode-bidi" version = "0.3.15" @@ -8527,6 +9196,16 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -8542,7 +9221,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", - "serde 1.0.203", + "serde 1.0.228", ] [[package]] @@ -8671,6 +9350,15 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "waitgroup" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1f50000a783467e6c0200f9d10642f4bc424e39efc1b770203e88b488f79292" +dependencies = [ + "atomic-waker", +] + [[package]] name = "waker-fn" version = "1.2.0" @@ -9005,6 +9693,175 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webrtc" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08fd686c0920ac08f3a57eacc48e31f0e4ca1ffefba4478784606f78c14e83ad" +dependencies = [ + "arc-swap", + "async-trait", + "bytes", + "dtls", + "hex", + "interceptor", + "lazy_static", + "log", + "portable-atomic", + "rand 0.9.2", + "rcgen", + "regex", + "ring", + "rtcp", + "rtp", + "sdp", + "serde 1.0.228", + "serde_json 1.0.118", + "sha2", + "smol_str", + "stun", + "thiserror 1.0.61", + "tokio", + "turn", + "unicase", + "url", + "waitgroup", + "webrtc-data", + "webrtc-ice", + "webrtc-mdns", + "webrtc-media", + "webrtc-sctp", + "webrtc-srtp", + "webrtc-util", +] + +[[package]] +name = "webrtc-data" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "062a5438d63bb0756a221693d76cc0dd6119affee1dfdfe57abe3a2a8c8b3eea" +dependencies = [ + "bytes", + "log", + "portable-atomic", + "thiserror 1.0.61", + "tokio", + "webrtc-sctp", + "webrtc-util", +] + +[[package]] +name = "webrtc-ice" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cb13fd1a373e68addc4bba0c8ca058627518e54342583d024bdcbb8ae5d97d" +dependencies = [ + "arc-swap", + "async-trait", + "crc", + "log", + "portable-atomic", + "rand 0.9.2", + "serde 1.0.228", + "serde_json 1.0.118", + "stun", + "thiserror 1.0.61", + "tokio", + "turn", + "url", + "uuid", + "waitgroup", + "webrtc-mdns", + "webrtc-util", +] + +[[package]] +name = "webrtc-mdns" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17279a067e75df72ce923fdeb7f04cd808f6f5aa4910dc6bcb4fbe66b396ace" +dependencies = [ + "log", + "socket2 0.5.10", + "thiserror 1.0.61", + "tokio", + "webrtc-util", +] + +[[package]] +name = "webrtc-media" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94a84c910fec0848fd5a0d8a5651e0ddbdedaf25a7d3ae3f0b15f71ac73a1773" +dependencies = [ + "byteorder", + "bytes", + "rand 0.9.2", + "rtp", + "thiserror 1.0.61", +] + +[[package]] +name = "webrtc-sctp" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f985465467d8910c1f8ac4382cd64f83b1f6a1a75021a82b221546f6fb3b856f" +dependencies = [ + "arc-swap", + "async-trait", + "bytes", + "crc", + "log", + "portable-atomic", + "rand 0.9.2", + "thiserror 1.0.61", + "tokio", + "webrtc-util", +] + +[[package]] +name = "webrtc-srtp" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d8cdc33413f1d0192670a80ce93d17cb78d57fe3a2414be30d6f6dff121123" +dependencies = [ + "aead", + "aes", + "aes-gcm", + "byteorder", + "bytes", + "ctr", + "hmac", + "log", + "rtcp", + "rtp", + "sha1", + "subtle", + "thiserror 1.0.61", + "tokio", + "webrtc-util", +] + +[[package]] +name = "webrtc-util" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c0c7e0c8f280f2bbfae442701465777ac07adaf46ce0c5863cd58e13fe472a" +dependencies = [ + "async-trait", + "bitflags 1.3.2", + "bytes", + "ipnet", + "lazy_static", + "log", + "nix 0.26.4", + "portable-atomic", + "rand 0.9.2", + "thiserror 1.0.61", + "tokio", + "winapi 0.3.9", +] + [[package]] name = "weezl" version = "0.1.8" @@ -9878,6 +10735,36 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde 1.0.228", + "zeroize", +] + +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror 1.0.61", + "time 0.3.36", +] + [[package]] name = "xattr" version = "1.4.0" @@ -9924,6 +10811,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time 0.3.36", +] + [[package]] name = "zbus" version = "3.15.2" @@ -9952,7 +10848,7 @@ dependencies = [ "once_cell", "ordered-stream", "rand 0.8.5", - "serde 1.0.203", + "serde 1.0.228", "serde_repr", "sha1", "static_assertions", @@ -9985,7 +10881,7 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "437d738d3750bed6ca9b8d423ccc7a8eb284f6b1d6d4e225a0e4e6258d864c8d" dependencies = [ - "serde 1.0.203", + "serde 1.0.228", "static_assertions", "zvariant", ] @@ -10036,6 +10932,20 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] [[package]] name = "zip" @@ -10122,7 +11032,7 @@ dependencies = [ "byteorder", "enumflags2", "libc", - "serde 1.0.203", + "serde 1.0.228", "static_assertions", "zvariant_derive", ] diff --git a/build.rs b/build.rs index 672f972d9..92fb1f4b4 100644 --- a/build.rs +++ b/build.rs @@ -18,7 +18,7 @@ fn build_mac() { b.flag("-DNO_InputMonitoringAuthStatus=1"); } } - b.file(file).compile("macos"); + b.flag("-std=c++17").file(file).compile("macos"); println!("cargo:rerun-if-changed={}", file); } diff --git a/libs/hbb_common b/libs/hbb_common index a86eda749..8b0e25867 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit a86eda749e6fa33c282bab680e6b504d3ad87539 +Subproject commit 8b0e25867375ba9e6bff548acf44fe6d6ffa7c0e From 9cfa551163c043f12b3af77577901f2da5f25cfd Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 28 Nov 2025 17:25:43 +0800 Subject: [PATCH 012/277] fix: msi, prevent black window (#13665) For msi version, the black window is shown when creating desktop shortcut for connection. The exe version does not have this issue. Signed-off-by: fufesou --- src/platform/windows.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index b5663c26c..9481bd69f 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -1882,6 +1882,7 @@ oLink.Save .to_owned(); std::process::Command::new("cscript") .arg(&shortcut) + .creation_flags(CREATE_NO_WINDOW) .output()?; allow_err!(std::fs::remove_file(shortcut)); Ok(()) From 8e6e91eb4a59d0af24c920aef62c0d5e37fc9c56 Mon Sep 17 00:00:00 2001 From: bilimiyorum <131397022+bilimiyorum@users.noreply.github.com> Date: Sun, 30 Nov 2025 14:52:40 +0300 Subject: [PATCH 013/277] Turkish language support (#13673) Current --- src/lang/tr.rs | 458 ++++++++++++++++++++++++------------------------- 1 file changed, 229 insertions(+), 229 deletions(-) diff --git a/src/lang/tr.rs b/src/lang/tr.rs index cc4ccc0e7..48efb04fa 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -37,18 +37,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Kopyalanan geçici veri boş"), ("Stop service", "Servisi Durdur"), ("Change ID", "ID Değiştir"), - ("Your new ID", ""), - ("length %min% to %max%", ""), - ("starts with a letter", ""), - ("allowed characters", ""), + ("Your new ID", "Yeni ID'niz"), + ("length %min% to %max%", "uzunluk %min% ila %max%"), + ("starts with a letter", "bir harfle başlar"), + ("allowed characters", "izin verilen karakterler"), ("id_change_tip", "Yalnızca a-z, A-Z, 0-9, - (dash) ve _ (alt çizgi) karakterlerini kullanabilirsiniz. İlk karakter a-z veya A-Z olmalıdır. Uzunluk 6 ile 16 karakter arasında olmalıdır."), ("Website", "Website"), ("About", "Hakkında"), - ("Slogan_tip", ""), - ("Privacy Statement", ""), + ("Slogan_tip", "Bu kaotik dünyada gönülden yapıldı!"), + ("Privacy Statement", "Gizlilik Beyanı"), ("Mute", "Sustur"), - ("Build Date", ""), - ("Version", ""), + ("Build Date", "Yapım Tarihi"), + ("Version", "Sürüm"), ("Home", ""), ("Audio Input", "Ses Girişi"), ("Enhancements", "Geliştirmeler"), @@ -212,11 +212,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Always connect via relay"), ("whitelist_tip", "Bu masaüstüne yalnızca yetkili IP adresleri bağlanabilir"), ("Login", "Giriş yap"), - ("Verify", ""), - ("Remember me", ""), - ("Trust this device", ""), - ("Verification code", ""), - ("verification_tip", ""), + ("Verify", "Doğrula"), + ("Remember me", "Beni hatırla"), + ("Trust this device", "Bu cihaza güvenin"), + ("Verification code", "Doğrulama kodu"), + ("verification_tip", "doğrulama tipi"), ("Logout", "Çıkış yap"), ("Tags", "Etiketler"), ("Search ID", "ID Arama"), @@ -228,7 +228,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Username missed", "Kullanıcı adı boş"), ("Password missed", "Şifre boş"), ("Wrong credentials", "Yanlış kimlik bilgileri"), - ("The verification code is incorrect or has expired", ""), + ("The verification code is incorrect or has expired", "Doğrulama kodu hatalı veya süresi dolmuş"), ("Edit Tag", "Etiketi düzenle"), ("Forget Password", "Şifreyi Unut"), ("Favorites", "Favoriler"), @@ -282,8 +282,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_service_will_start_tip", "Ekran Yakalamanın etkinleştirilmesi, hizmeti otomatik olarak başlatacak ve diğer cihazların bu cihazdan bağlantı talep etmesine izin verecektir."), ("android_stop_service_tip", "Hizmetin kapatılması, kurulan tüm bağlantıları otomatik olarak kapatacaktır."), ("android_version_audio_tip", "Mevcut Android sürümü ses yakalamayı desteklemiyor, lütfen Android 10 veya sonraki bir sürüme yükseltin."), - ("android_start_service_tip", ""), - ("android_permission_may_not_change_tip", ""), + ("android_start_service_tip", "Ekran paylaşım hizmetini başlatmak için [Hizmeti başlat] ögesine dokunun veya [Ekran Görüntüsü] iznini etkinleştirin."), + ("android_permission_may_not_change_tip", "Kurulan bağlantılara ait izinler, yeniden bağlantı kurulana kadar anında değiştirilemez."), ("Account", "Hesap"), ("Overwrite", "üzerine yaz"), ("This file exists, skip or overwrite this file?", "Bu dosya var, bu dosya atlansın veya üzerine yazılsın mı?"), @@ -301,10 +301,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Language", "Dil"), ("Keep RustDesk background service", "RustDesk arka plan hizmetini sürdürün"), ("Ignore Battery Optimizations", "Pil Optimizasyonlarını Yoksay"), - ("android_open_battery_optimizations_tip", ""), - ("Start on boot", ""), - ("Start the screen sharing service on boot, requires special permissions", ""), - ("Connection not allowed", "bağlantıya izin verilmedi"), + ("android_open_battery_optimizations_tip", "Bu özelliği devre dışı bırakmak istiyorsanız lütfen bir sonraki RustDesk uygulama ayarları sayfasına gidin, [Pil] ögesini bulun ve girin, [Sınırsız] ögesinin işaretini kaldırın"), + ("Start on boot", "Önyüklemede başla"), + ("Start the screen sharing service on boot, requires special permissions", "Ekran paylaşım hizmetini önyüklemede başlatmak için özel izinler gerekir"), + ("Connection not allowed", "Bağlantıya izin verilmedi"), ("Legacy mode", "Eski mod"), ("Map mode", "Haritalama modu"), ("Translate mode", "Çeviri modu"), @@ -315,7 +315,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restart remote device", "Uzaktaki cihazı yeniden başlat"), ("Are you sure you want to restart", "Yeniden başlatmak istediğinize emin misin?"), ("Restarting remote device", "Uzaktan yeniden başlatılıyor"), - ("remote_restarting_tip", "remote_restarting_tip"), + ("remote_restarting_tip", "Uzak cihaz yeniden başlatılıyor, lütfen bu mesaj kutusunu kapatın ve bir süre sonra kalıcı şifre ile yeniden bağlanın"), ("Copied", "Kopyalandı"), ("Exit Fullscreen", "Tam ekrandan çık"), ("Fullscreen", "Tam ekran"), @@ -326,19 +326,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Oran"), ("Image Quality", "Görüntü kalitesi"), ("Scroll Style", "Kaydırma Stili"), - ("Show Toolbar", ""), - ("Hide Toolbar", ""), + ("Show Toolbar", "Araç Çubuğunu Göster"), + ("Hide Toolbar", "Araç Çubuğunu Gizle"), ("Direct Connection", "Doğrudan Bağlantı"), ("Relay Connection", "Röle Bağlantısı"), - ("Secure Connection", "Güvenli bağlantı"), - ("Insecure Connection", "Güvenli Bağlantı"), + ("Secure Connection", "Güvenli Bağlantı"), + ("Insecure Connection", "Güvenli Olmayan Bağlantı"), ("Scale original", "Orijinali ölçeklendir"), ("Scale adaptive", "Ölçek uyarlanabilir"), ("General", "Genel"), ("Security", "Güvenlik"), ("Theme", "Tema"), ("Dark Theme", "Koyu Tema"), - ("Light Theme", ""), + ("Light Theme", "Açık Tema"), ("Dark", "Koyu"), ("Light", "Açık"), ("Follow System", "Sisteme Uy"), @@ -355,12 +355,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Audio Input Device", "Ses Giriş Aygıtı"), ("Use IP Whitelisting", "IP Beyaz Listeyi Kullan"), ("Network", "Ağ"), - ("Pin Toolbar", ""), - ("Unpin Toolbar", ""), + ("Pin Toolbar", "Araç Çubuğunu Sabitle"), + ("Unpin Toolbar", "Araç Çubuğunu Sabitlemeyi Kaldır"), ("Recording", "Kayıt Ediliyor"), ("Directory", "Klasör"), - ("Automatically record incoming sessions", "Gelen oturumları otomatik olarak kayıt et"), - ("Automatically record outgoing sessions", ""), + ("Automatically record incoming sessions", "Gelen oturumları otomatik olarak kaydet"), + ("Automatically record outgoing sessions", "Giden oturumları otomatik olarak kaydet"), ("Change", "Değiştir"), ("Start session recording", "Oturum kaydını başlat"), ("Stop session recording", "Oturum kaydını sonlandır"), @@ -526,208 +526,208 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change Color", "Rengi Değiştir"), ("Primary Color", "Birincil Renk"), ("HSV Color", "HSV Rengi"), - ("Installation Successful!", ""), - ("Installation failed!", ""), - ("Reverse mouse wheel", ""), - ("{} sessions", ""), - ("scam_title", ""), - ("scam_text1", ""), - ("scam_text2", ""), - ("Don't show again", ""), - ("I Agree", ""), - ("Decline", ""), - ("Timeout in minutes", ""), - ("auto_disconnect_option_tip", ""), - ("Connection failed due to inactivity", ""), - ("Check for software update on startup", ""), - ("upgrade_rustdesk_server_pro_to_{}_tip", ""), - ("pull_group_failed_tip", ""), - ("Filter by intersection", ""), - ("Remove wallpaper during incoming sessions", ""), - ("Test", ""), - ("display_is_plugged_out_msg", ""), - ("No displays", ""), - ("Open in new window", ""), - ("Show displays as individual windows", ""), - ("Use all my displays for the remote session", ""), - ("selinux_tip", ""), - ("Change view", ""), - ("Big tiles", ""), - ("Small tiles", ""), - ("List", ""), - ("Virtual display", ""), - ("Plug out all", ""), - ("True color (4:4:4)", ""), - ("Enable blocking user input", ""), - ("id_input_tip", ""), - ("privacy_mode_impl_mag_tip", ""), - ("privacy_mode_impl_virtual_display_tip", ""), - ("Enter privacy mode", ""), - ("Exit privacy mode", ""), - ("idd_not_support_under_win10_2004_tip", ""), - ("input_source_1_tip", ""), - ("input_source_2_tip", ""), - ("Swap control-command key", ""), - ("swap-left-right-mouse", ""), - ("2FA code", ""), - ("More", ""), - ("enable-2fa-title", ""), - ("enable-2fa-desc", ""), - ("wrong-2fa-code", ""), - ("enter-2fa-title", ""), - ("Email verification code must be 6 characters.", ""), - ("2FA code must be 6 digits.", ""), - ("Multiple Windows sessions found", ""), - ("Please select the session you want to connect to", ""), - ("powered_by_me", ""), - ("outgoing_only_desk_tip", ""), - ("preset_password_warning", ""), - ("Security Alert", ""), - ("My address book", ""), - ("Personal", ""), - ("Owner", ""), - ("Set shared password", ""), - ("Exist in", ""), - ("Read-only", ""), - ("Read/Write", ""), - ("Full Control", ""), - ("share_warning_tip", ""), - ("Everyone", ""), - ("ab_web_console_tip", ""), - ("allow-only-conn-window-open-tip", ""), - ("no_need_privacy_mode_no_physical_displays_tip", ""), - ("Follow remote cursor", ""), - ("Follow remote window focus", ""), + ("Installation Successful!", "Kurulum Başarılı!"), + ("Installation failed!", "Kurulum başarısız!"), + ("Reverse mouse wheel", "Ters fare tekerleği"), + ("{} sessions", "{} oturum"), + ("scam_title", "Dolandırılıyor Olabilirsiniz!"), + ("scam_text1", "Eğer tanımadığınız ve güvenmediğiniz birisiyle telefonda konuşuyorsanız ve sizden RustDesk'i kullanmanızı ve hizmeti başlatmanızı istiyorsa devam etmeyin ve hemen telefonu kapatın."), + ("scam_text2", "Muhtemelen paranızı veya diğer özel bilgilerinizi çalmaya çalışan dolandırıcılardır."), + ("Don't show again", "Bir daha gösterme"), + ("I Agree", "Kabul ediyorum"), + ("Decline", "Reddet"), + ("Timeout in minutes", "Zaman aşımı (dakika)"), + ("auto_disconnect_option_tip", "Kullanıcı etkin olmadığında gelen oturumları otomatik olarak kapat"), + ("Connection failed due to inactivity", "Etkin olmama nedeniyle otomatik olarak bağlantı kesildi"), + ("Check for software update on startup", "Başlangıçta yazılım güncellemesini kontrol et"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Lütfen RustDesk Server Pro'yu {} veya daha yeni bir sürüme yükseltin!"), + ("pull_group_failed_tip", "Grup yenilenemedi"), + ("Filter by intersection", "Kesişim noktasına göre filtrele"), + ("Remove wallpaper during incoming sessions", "Gelen oturumlar sırasında duvar kağıdını kaldır"), + ("Test", "Test"), + ("display_is_plugged_out_msg", "Ekran fişi çekilmiş, ilk ekrana geç."), + ("No displays", "Görüntü yok"), + ("Open in new window", "Yeni pencerede aç"), + ("Show displays as individual windows", "Ekranları ayrı pencereler olarak göster"), + ("Use all my displays for the remote session", "Uzak oturum için tüm ekranlarımı kullan"), + ("selinux_tip", "Cihazınızda SELinux etkin olduğundan, RustDesk'in kontrollü tarafta düzgün çalışmasını engelleyebilir."), + ("Change view", "Görünümü değiştir"), + ("Big tiles", "Büyük döşemeler"), + ("Small tiles", "Küçük döşemeler"), + ("List", "Liste"), + ("Virtual display", "Sanal ekran"), + ("Plug out all", "Tümünü çıkar"), + ("True color (4:4:4)", "Gerçek renk (4:4:4)"), + ("Enable blocking user input", "Kullanıcı girişini engellemeyi etkinleştir"), + ("id_input_tip", "Bir ID, doğrudan IP veya portlu bir etki alanı (:) girebilirsiniz.\nBaşka bir sunucudaki bir cihaza erişmek istiyorsanız lütfen sunucu adresini (@?key=) ekleyin, örneğin,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nGenel bir sunucudaki bir cihaza erişmek istiyorsanız lütfen \"@public\" girin, genel sunucu için anahtara gerek yoktur.\n\nİlk bağlantıda bir röle bağlantısının kullanılmasını zorlamak istiyorsanız ID'nin sonuna \"/r\" ekleyin, örneğin, \"9123456234/r\"."), + ("privacy_mode_impl_mag_tip", "Mod 1"), + ("privacy_mode_impl_virtual_display_tip", "Mod 2"), + ("Enter privacy mode", "Gizlilik moduna gir"), + ("Exit privacy mode", "Gizlilik modundan çık"), + ("idd_not_support_under_win10_2004_tip", "Dolaylı ekran sürücüsü desteklenmiyor. Windows 10, sürüm 2004 veya daha yenisi gereklidir."), + ("input_source_1_tip", "Giriş kaynağı 1"), + ("input_source_2_tip", "Giriş kaynağı 2"), + ("Swap control-command key", "Kontrol-komut tuşunu değiştir"), + ("swap-left-right-mouse", "sol-sağ fareyi değiştir"), + ("2FA code", "2FA kodu"), + ("More", "Daha"), + ("enable-2fa-title", "İki faktörlü kimlik doğrulamayı etkinleştir"), + ("enable-2fa-desc", "Lütfen kimlik doğrulayıcınızı şimdi kurun. Telefonunuzda veya masaüstünüzde Authy, Microsoft veya Google Authenticator gibi bir kimlik doğrulayıcı uygulaması kullanabilirsiniz. İki faktörlü kimlik doğrulamayı etkinleştirmek için QR kodunu uygulamanızla tarayın ve uygulamanızın gösterdiği kodu girin."), + ("wrong-2fa-code", "Kod doğrulanamıyor. Kod ve yerel saat ayarlarının doğru olduğundan emin olun."), + ("enter-2fa-title", "İki faktörlü kimlik doğrulama"), + ("Email verification code must be 6 characters.", "E-posta doğrulama kodu 6 karakterden oluşmalıdır."), + ("2FA code must be 6 digits.", "2FA kodu 6 haneli olmalıdır."), + ("Multiple Windows sessions found", "Birden fazla Windows oturumu bulundu"), + ("Please select the session you want to connect to", "Lütfen bağlanmak istediğiniz oturumu seçin"), + ("powered_by_me", "RustDesk tarafından desteklenmektedir"), + ("outgoing_only_desk_tip", "Bu özelleştirilmiş bir sürümdür.\nDiğer cihazlara bağlanabilirsiniz, ancak diğer cihazlar cihazınıza bağlanamaz."), + ("preset_password_warning", "Bu özelleştirilmiş sürüm, önceden ayarlanmış bir şifre ile birlikte gelir. Bu parolayı bilen herkes cihazınızın tam kontrolünü ele geçirebilir. Bunu beklemiyorsanız yazılımı hemen kaldırın."), + ("Security Alert", "Güvenlik Uyarısı"), + ("My address book", "Adres defterim"), + ("Personal", "Kişisel"), + ("Owner", "Sahip"), + ("Set shared password", "Paylaşılan şifreyi ayarla"), + ("Exist in", "İçinde varolan"), + ("Read-only", "Salt okunur"), + ("Read/Write", "Okuma/Yazma"), + ("Full Control", "Tam Kontrol"), + ("share_warning_tip", "Yukarıdaki alanlar paylaşılır ve başkaları tarafından görülebilir"), + ("Everyone", "Herkes"), + ("ab_web_console_tip", "Web konsolu hakkında daha fazla bilgi"), + ("allow-only-conn-window-open-tip", "Yalnızca RustDesk penceresi açıksa bağlantıya izin ver"), + ("no_need_privacy_mode_no_physical_displays_tip", "Fiziksel ekran yok, gizlilik modunu kullanmaya gerek yok."), + ("Follow remote cursor", "Uzak imleci takip et"), + ("Follow remote window focus", "Uzak pencere odağını takip et"), ("default_proxy_tip", ""), - ("no_audio_input_device_tip", ""), - ("Incoming", ""), - ("Outgoing", ""), - ("Clear Wayland screen selection", ""), - ("clear_Wayland_screen_selection_tip", ""), - ("confirm_clear_Wayland_screen_selection_tip", ""), - ("android_new_voice_call_tip", ""), - ("texture_render_tip", ""), - ("Use texture rendering", ""), - ("Floating window", ""), - ("floating_window_tip", ""), - ("Keep screen on", ""), - ("Never", ""), - ("During controlled", ""), - ("During service is on", ""), - ("Capture screen using DirectX", ""), - ("Back", ""), - ("Apps", ""), - ("Volume up", ""), - ("Volume down", ""), - ("Power", ""), - ("Telegram bot", ""), - ("enable-bot-tip", ""), - ("enable-bot-desc", ""), - ("cancel-2fa-confirm-tip", ""), - ("cancel-bot-confirm-tip", ""), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), - ("one-way-file-transfer-tip", ""), - ("Authentication Required", ""), - ("Authenticate", ""), - ("web_id_input_tip", ""), - ("Download", ""), - ("Upload folder", ""), - ("Upload files", ""), - ("Clipboard is synchronized", ""), - ("Update client clipboard", ""), - ("Untagged", ""), - ("new-version-of-{}-tip", ""), - ("Accessible devices", ""), - ("upgrade_remote_rustdesk_client_to_{}_tip", ""), - ("d3d_render_tip", ""), - ("Use D3D rendering", ""), - ("Printer", ""), - ("printer-os-requirement-tip", ""), - ("printer-requires-installed-{}-client-tip", ""), - ("printer-{}-not-installed-tip", ""), - ("printer-{}-ready-tip", ""), - ("Install {} Printer", ""), - ("Outgoing Print Jobs", ""), - ("Incoming Print Jobs", ""), - ("Incoming Print Job", ""), - ("use-the-default-printer-tip", ""), - ("use-the-selected-printer-tip", ""), - ("auto-print-tip", ""), - ("print-incoming-job-confirm-tip", ""), - ("remote-printing-disallowed-tile-tip", ""), - ("remote-printing-disallowed-text-tip", ""), - ("save-settings-tip", ""), - ("dont-show-again-tip", ""), - ("Take screenshot", ""), - ("Taking screenshot", ""), - ("screenshot-merged-screen-not-supported-tip", ""), - ("screenshot-action-tip", ""), - ("Save as", ""), - ("Copy to clipboard", ""), - ("Enable remote printer", ""), - ("Downloading {}", ""), - ("{} Update", ""), - ("{}-to-update-tip", ""), - ("download-new-version-failed-tip", ""), - ("Auto update", ""), - ("update-failed-check-msi-tip", ""), - ("websocket_tip", ""), - ("Use WebSocket", ""), - ("Trackpad speed", ""), - ("Default trackpad speed", ""), - ("Numeric one-time password", ""), - ("Enable IPv6 P2P connection", ""), - ("Enable UDP hole punching", ""), + ("no_audio_input_device_tip", "Varsayılan protokol ve port, Socks5 ve 1080'dir"), + ("Incoming", "Gelen"), + ("Outgoing", "Giden"), + ("Clear Wayland screen selection", "Wayland ekran seçimini temizle"), + ("clear_Wayland_screen_selection_tip", "Ekran seçimini temizledikten sonra paylaşılacak ekranı tekrar seçebilirsiniz."), + ("confirm_clear_Wayland_screen_selection_tip", "Wayland ekran seçimini temizlemek istediğinizden emin misiniz?"), + ("android_new_voice_call_tip", "Yeni bir sesli arama isteği alındı. Kabul ederseniz sesli iletişime geçilecektir."), + ("texture_render_tip", "Resimleri daha pürüzsüz hale getirmek için doku oluşturmayı kullanın. Oluşturma sorunlarıyla karşılaşırsanız bu seçeneği devre dışı bırakmayı deneyebilirsiniz."), + ("Use texture rendering", "Doku oluşturmayı kullan"), + ("Floating window", "Yüzen pencere"), + ("floating_window_tip", "RustDesk arka plan hizmetini açık tutmaya yardımcı olur"), + ("Keep screen on", "Ekranı açık tut"), + ("Never", "Asla"), + ("During controlled", "Kontrol sırasınd"), + ("During service is on", "Servis açıkken"), + ("Capture screen using DirectX", "DirectX kullanarak ekran görüntüsü al"), + ("Back", "Geri"), + ("Apps", "Uygulamalar"), + ("Volume up", "Sesi yükselt"), + ("Volume down", "Sesi azalt"), + ("Power", "Güç"), + ("Telegram bot", "Telegram bot"), + ("enable-bot-tip", "Bu özelliği etkinleştirirseniz botunuzdan 2FA kodunu alabilirsiniz. Aynı zamanda bağlantı bildirimi işlevi de görebilir."), + ("enable-bot-desc", "1. @BotFather ile bir sohbet açın.\n2. \"/newbot\" komutunu gönderin. Bu adımı tamamladıktan sonra bir jeton alacaksınız.\n3. Yeni oluşturduğunuz botla bir sohbet başlatın. Etkinleştirmek için eğik çizgiyle (\"/\") başlayan \"/merhaba\" gibi bir mesaj gönderin.\n"), + ("cancel-2fa-confirm-tip", "2FA'yı iptal etmek istediğinizden emin misiniz?"), + ("cancel-bot-confirm-tip", "Telegram botunu iptal etmek istediğinizden emin misiniz?"), + ("About RustDesk", "RustDesk Hakkında"), + ("Send clipboard keystrokes", "Panoya tuş vuruşlarını gönder"), + ("network_error_tip", "Lütfen ağ bağlantınızı kontrol edin ve ardından yeniden dene'ye tıklayın."), + ("Unlock with PIN", "PIN ile kilidi açın"), + ("Requires at least {} characters", "En az {} karakter gerektirir"), + ("Wrong PIN", "Yanlış PIN"), + ("Set PIN", "PIN'i ayarla"), + ("Enable trusted devices", "Güvenilir cihazları etkinleştir"), + ("Manage trusted devices", "Güvenilir cihazları yönet"), + ("Platform", "Platform"), + ("Days remaining", "Kalan gün sayısı"), + ("enable-trusted-devices-tip", "Güvenilir cihazlarda 2FA doğrulamasını atla"), + ("Parent directory", "Üst dizin"), + ("Resume", "Devam ettir"), + ("Invalid file name", "Geçersiz dosya adı"), + ("one-way-file-transfer-tip", "Kontrol edilen tarafta tek yönlü dosya transferi aktiftir."), + ("Authentication Required", "Kimlik Doğrulama Gerekli"), + ("Authenticate", "Kimlik doğrulaması"), + ("web_id_input_tip", "Aynı sunucuda bir kimlik girebilirsiniz, web istemcisinde doğrudan IP erişimi desteklenmez.\nBaşka bir sunucudaki bir cihaza erişmek istiyorsanız lütfen sunucu adresini (@?key=) ekleyin, örneğin,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nGenel bir sunucudaki bir cihaza erişmek istiyorsanız, lütfen \"@public\" girin, genel sunucu için anahtara gerek yoktur."), + ("Download", "İndir"), + ("Upload folder", "Klasör yükle"), + ("Upload files", "Dosya yükle"), + ("Clipboard is synchronized", "Pano senkronize edildi"), + ("Update client clipboard", "İstemci panosunu güncelle"), + ("Untagged", "Etiketsiz"), + ("new-version-of-{}-tip", "{}'nin yeni bir sürümü mevcut"), + ("Accessible devices", "Erişilebilir cihazlar"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Lütfen uzak tarafta RustDesk istemcisini {} sürümüne veya daha yenisine güncelleyin!"), + ("d3d_render_tip", "D3D oluşturma etkinleştirildiğinde, bazı bilgisayarlarda uzak kontrol ekranı siyah görünebilir."), + ("Use D3D rendering", "D3D oluşturmayı kullan"), + ("Printer", "Yazıcı"), + ("printer-os-requirement-tip", "Yazıcı çıkış fonksiyonu için Windows 10 veya üzeri gereklidir."), + ("printer-requires-installed-{}-client-tip", "Uzaktan yazdırmayı kullanabilmek için bu cihaza {} yüklenmesi gerekir."), + ("printer-{}-not-installed-tip", "{} Yazıcısı yüklü değil."), + ("printer-{}-ready-tip", "{} Yazıcısı kuruldu ve kullanıma hazır."), + ("Install {} Printer", "{} Yazıcısını Yükle"), + ("Outgoing Print Jobs", "Giden Baskı İşleri"), + ("Incoming Print Jobs", "Gelen Baskı İşleri"), + ("Incoming Print Job", "Gelen Baskı İşi"), + ("use-the-default-printer-tip", "Varsayılan yazıcıyı kullan"), + ("use-the-selected-printer-tip", "Seçili yazıcıyı kullan"), + ("auto-print-tip", "Seçili yazıcıyı kullanarak otomatik olarak yazdır."), + ("print-incoming-job-confirm-tip", "Uzak bir kaynaktan yazdırma işi aldınız. Bunu kendi tarafınızda çalıştırmak ister misiniz?"), + ("remote-printing-disallowed-tile-tip", "Uzak Yazdırma engellendi"), + ("remote-printing-disallowed-text-tip", "Kontrol edilen tarafın izin ayarları Uzak Yazdırmaya izin vermiyor."), + ("save-settings-tip", "Ayarları kaydet"), + ("dont-show-again-tip", "Bunu bir daha gösterme"), + ("Take screenshot", "Ekran görüntüsü al"), + ("Taking screenshot", "Ekran görüntüsü alınıyor"), + ("screenshot-merged-screen-not-supported-tip", "Birden fazla ekranın ekran görüntülerinin birleştirilmesi şu anda desteklenmiyor. Lütfen tek bir ekrana geçin ve tekrar deneyin."), + ("screenshot-action-tip", "Lütfen ekran görüntüsüyle nasıl devam edeceğinizi seçin."), + ("Save as", "Farklı kaydet"), + ("Copy to clipboard", "Panoya kopyala"), + ("Enable remote printer", "Uzak yazıcıyı etkinleştir"), + ("Downloading {}", "{} indiriliyor"), + ("{} Update", "{} Güncellemesi"), + ("{}-to-update-tip", "{} şimdi kapanacak ve yeni sürüm kurulacak."), + ("download-new-version-failed-tip", "İndirme başarısız oldu. Tekrar deneyebilir veya 'İndir' düğmesine tıklayarak sürüm sayfasından manuel olarak indirip güncelleyebilirsiniz."), + ("Auto update", "Otomatik güncelleme"), + ("update-failed-check-msi-tip", "Kurulum yöntemi denetimi başarısız oldu. Sürüm sayfasından indirmek ve manuel olarak yükseltmek için lütfen "İndir" düğmesine tıklayın."), + ("websocket_tip", "WebSocket kullanıldığında yalnızca röle bağlantıları desteklenir."), + ("Use WebSocket", "WebSocket'ı kullan"), + ("Trackpad speed", "İzleme paneli hızı"), + ("Default trackpad speed", "Varsayılan izleme paneli hızı"), + ("Numeric one-time password", "Sayısal tek seferlik şifre"), + ("Enable IPv6 P2P connection", "IPv6 P2P bağlantısını etkinleştir"), + ("Enable UDP hole punching", "UDP delik açmayı etkinleştir"), ("View camera", "Kamerayı görüntüle"), - ("Enable camera", ""), - ("No cameras", ""), - ("view_camera_unsupported_tip", ""), + ("Enable camera", "Kamerayı etkinleştir"), + ("No cameras", "Kamera yok"), + ("view_camera_unsupported_tip", "Uzak cihaz, kameranın görüntülenmesini desteklemiyor."), ("Terminal", ""), ("Enable terminal", ""), - ("New tab", ""), - ("Keep terminal sessions on disconnect", ""), - ("Terminal (Run as administrator)", ""), - ("terminal-admin-login-tip", ""), - ("Failed to get user token.", ""), - ("Incorrect username or password.", ""), - ("The user is not an administrator.", ""), - ("Failed to check if the user is an administrator.", ""), - ("Supported only in the installed version.", ""), - ("elevation_username_tip", ""), - ("Preparing for installation ...", ""), - ("Show my cursor", ""), - ("Scale custom", ""), - ("Custom scale slider", ""), - ("Decrease", ""), - ("Increase", ""), - ("Show virtual mouse", ""), - ("Virtual mouse size", ""), - ("Small", ""), - ("Large", ""), - ("Show virtual joystick", ""), - ("Edit note", ""), - ("Alias", ""), - ("ScrollEdge", ""), - ("Allow insecure TLS fallback", ""), - ("allow-insecure-tls-fallback-tip", ""), - ("Disable UDP", ""), - ("disable-udp-tip", ""), - ("server-oss-not-support-tip", ""), - ("input note here", ""), - ("note-at-conn-end-tip", ""), + ("New tab", "Yeni sekme"), + ("Keep terminal sessions on disconnect", "Bağlantı kesildiğinde uçbirim oturumlarını açık tut"), + ("Terminal (Run as administrator)", "Terminal (Yönetici olarak çalıştır)"), + ("terminal-admin-login-tip", "Lütfen kontrol edilen tarafın yönetici kullanıcı adı ve şifresini giriniz."), + ("Failed to get user token.", "Kullanıcı belirteci alınamadı."), + ("Incorrect username or password.", "Hatalı kullanıcı adı veya şifre."), + ("The user is not an administrator.", "Kullanıcı bir yönetici değil."), + ("Failed to check if the user is an administrator.", "Kullanıcının yönetici olup olmadığı kontrol edilemedi."), + ("Supported only in the installed version.", "Sadece yüklü sürümde desteklenir."), + ("elevation_username_tip", "Kullanıcı adı veya etki alanı\\kullanıcı adı girin"), + ("Preparing for installation ...", "Kuruluma hazırlanıyor..."), + ("Show my cursor", "İmlecimi göster"), + ("Scale custom", "Özel boyutlandır"), + ("Custom scale slider", "Özel ölçek kaydırıcısı"), + ("Decrease", "Azalt"), + ("Increase", "Arttır"), + ("Show virtual mouse", "Sanal fareyi göster"), + ("Virtual mouse size", "Sanal fare boyutu"), + ("Small", "Küçük"), + ("Large", "Büyük"), + ("Show virtual joystick", "Sanal joystiği göster"), + ("Edit note", "Notu düzenle"), + ("Alias", "Takma ad"), + ("ScrollEdge", "Kaydırma kenarı"), + ("Allow insecure TLS fallback", "Güvensiz TLS geri dönüşüne izin ver"), + ("allow-insecure-tls-fallback-tip", "Varsayılan olarak, RustDesk sunucu sertifikasını TLS kullanarak protokoller için doğrular.\nBu seçenek etkinleştirildiğinde, doğrulama başarısızlığı durumunda RustDesk doğrulama adımını atlayarak işleme devam eder."), + ("Disable UDP", "UDP'yi devre dışı bırak"), + ("disable-udp-tip", "Yalnızca TCP kullanılıp kullanılmayacağını kontrol eder.\nBu seçenek etkinleştirildiğinde, RustDesk artık UDP 21116'yı kullanmayacak, bunun yerine TCP 21116 kullanılacaktır."), + ("server-oss-not-support-tip", "NOT: RustDesk sunucu OSS'si bu özelliği içermemektedir."), + ("input note here", "Notu buraya girin"), + ("note-at-conn-end-tip", "Bağlantı bittiğinde not sorulsun"), ].iter().cloned().collect(); } From 23754630e8dfac8ee8f0f8ee17cda5c4f1897336 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 1 Dec 2025 19:41:55 +0800 Subject: [PATCH 014/277] fix build (#13686) Signed-off-by: fufesou --- src/lang/tr.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 48efb04fa..74cf5767c 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -684,7 +684,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("{}-to-update-tip", "{} şimdi kapanacak ve yeni sürüm kurulacak."), ("download-new-version-failed-tip", "İndirme başarısız oldu. Tekrar deneyebilir veya 'İndir' düğmesine tıklayarak sürüm sayfasından manuel olarak indirip güncelleyebilirsiniz."), ("Auto update", "Otomatik güncelleme"), - ("update-failed-check-msi-tip", "Kurulum yöntemi denetimi başarısız oldu. Sürüm sayfasından indirmek ve manuel olarak yükseltmek için lütfen "İndir" düğmesine tıklayın."), + ("update-failed-check-msi-tip", "Kurulum yöntemi denetimi başarısız oldu. Sürüm sayfasından indirmek ve manuel olarak yükseltmek için lütfen \"İndir\" düğmesine tıklayın."), ("websocket_tip", "WebSocket kullanıldığında yalnızca röle bağlantıları desteklenir."), ("Use WebSocket", "WebSocket'ı kullan"), ("Trackpad speed", "İzleme paneli hızı"), From a78a803a2267da3bb76f8ec93f0cc96972bded47 Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 2 Dec 2025 14:54:56 +0800 Subject: [PATCH 015/277] fix is_public (#13701) Signed-off-by: 21pages --- src/common.rs | 25 ++++++++++++++++++++++++- src/hbbs_http/sync.rs | 2 +- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/common.rs b/src/common.rs index 4ac3b6cd9..6decd2d04 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1051,7 +1051,7 @@ fn get_api_server_(api: String, custom: String) -> String { #[inline] pub fn is_public(url: &str) -> bool { - url.contains("rustdesk.com") + url.contains("rustdesk.com/") || url.ends_with("rustdesk.com") } pub fn get_udp_punch_enabled() -> bool { @@ -2405,4 +2405,27 @@ mod tests { Duration::from_nanos(0) ); } + + #[test] + fn test_is_public() { + // Test URLs containing "rustdesk.com/" + assert!(is_public("https://rustdesk.com/")); + assert!(is_public("https://www.rustdesk.com/")); + assert!(is_public("https://api.rustdesk.com/v1")); + assert!(is_public("https://rustdesk.com/path")); + + // Test URLs ending with "rustdesk.com" + assert!(is_public("rustdesk.com")); + assert!(is_public("https://rustdesk.com")); + assert!(is_public("http://www.rustdesk.com")); + assert!(is_public("https://api.rustdesk.com")); + + // Test non-public URLs + assert!(!is_public("https://example.com")); + assert!(!is_public("https://custom-server.com")); + assert!(!is_public("http://192.168.1.1")); + assert!(!is_public("localhost")); + assert!(!is_public("https://rustdesk.computer.com")); + assert!(!is_public("rustdesk.comhello.com")); + } } diff --git a/src/hbbs_http/sync.rs b/src/hbbs_http/sync.rs index a266829a6..d3083acd1 100644 --- a/src/hbbs_http/sync.rs +++ b/src/hbbs_http/sync.rs @@ -278,7 +278,7 @@ fn heartbeat_url() -> String { Config::get_option("api-server"), Config::get_option("custom-rendezvous-server"), ); - if url.is_empty() || url.contains("rustdesk.com") { + if url.is_empty() || crate::is_public(&url) { return "".to_owned(); } format!("{}/api/heartbeat", url) From a342941ec1b2a01cc6cb7bb952157f66e7522aa5 Mon Sep 17 00:00:00 2001 From: Alex Rijckaert Date: Wed, 3 Dec 2025 17:27:05 +0100 Subject: [PATCH 016/277] Update Dutch translations for input notes (#13713) --- src/lang/nl.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/nl.rs b/src/lang/nl.rs index e449c25d5..50227384e 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -727,7 +727,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Disable UDP", "UDP uitschakelen"), ("disable-udp-tip", "Controleert of alleen TCP moet worden gebruikt. Als deze optie is ingeschakeld, gebruikt RustDesk niet langer UDP 21116, maar TCP 21116."), ("server-oss-not-support-tip", "Opmerking: Deze functie is niet beschikbaar in de open-sourceversie van de RustDesk-server."), - ("input note here", ""), - ("note-at-conn-end-tip", ""), + ("input note here", "voeg hier een opmerking toe"), + ("note-at-conn-end-tip", "Vraag om een opmerking aan het einde van de verbinding"), ].iter().cloned().collect(); } From 20ce626654630f04bacd405444f3e4349aa5bf95 Mon Sep 17 00:00:00 2001 From: Vasyl Gello Date: Thu, 4 Dec 2025 11:54:07 +0200 Subject: [PATCH 017/277] Fix OpenSSL build with Android NDK clang on x86 (#13684) Signed-off-by: Vasyl Gello --- flutter/ndk_x86.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/flutter/ndk_x86.sh b/flutter/ndk_x86.sh index 617c25f65..57e121274 100755 --- a/flutter/ndk_x86.sh +++ b/flutter/ndk_x86.sh @@ -1,2 +1,10 @@ #!/usr/bin/env bash + +# +# Fix OpenSSL build with Android NDK clang on 32-bit architectures +# + +export CFLAGS="-DBROKEN_CLANG_ATOMICS" +export CXXFLAGS="-DBROKEN_CLANG_ATOMICS" + cargo ndk --platform 21 --target i686-linux-android build --release --features flutter From eb0174ea536479725bc36b023f7c4be769d9e1b8 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 5 Dec 2025 17:15:29 +0800 Subject: [PATCH 018/277] flatpak command line is_root --- src/core_main.rs | 4 ++++ src/platform/linux.rs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core_main.rs b/src/core_main.rs index 9abfcb444..a4b9ecf1c 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -803,6 +803,10 @@ fn is_root() -> bool { return crate::platform::is_elevated(None).unwrap_or_default() || crate::platform::is_root(); } + #[cfg(linux)] + { + return crate::platform::is_flatpak() || crate::platform::is_root(); + } #[allow(unreachable_code)] crate::platform::is_root() } diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 66eefb8a2..07ec97d6e 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -701,7 +701,7 @@ pub fn get_env_var(k: &str) -> String { } } -fn is_flatpak() -> bool { +pub fn is_flatpak() -> bool { std::path::PathBuf::from("/.flatpak-info").exists() } From 4f4da20fc01b60d710748f97e09d24cfb0a74a07 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 5 Dec 2025 17:26:06 +0800 Subject: [PATCH 019/277] revert: flatpak command line is_root --- src/core_main.rs | 4 ---- src/platform/linux.rs | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/core_main.rs b/src/core_main.rs index a4b9ecf1c..9abfcb444 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -803,10 +803,6 @@ fn is_root() -> bool { return crate::platform::is_elevated(None).unwrap_or_default() || crate::platform::is_root(); } - #[cfg(linux)] - { - return crate::platform::is_flatpak() || crate::platform::is_root(); - } #[allow(unreachable_code)] crate::platform::is_root() } diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 07ec97d6e..66eefb8a2 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -701,7 +701,7 @@ pub fn get_env_var(k: &str) -> String { } } -pub fn is_flatpak() -> bool { +fn is_flatpak() -> bool { std::path::PathBuf::from("/.flatpak-info").exists() } From 0065085ba2c06fda7ba917c6543b96ce1937d5da Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:34:05 +0800 Subject: [PATCH 020/277] fix: win, peer shortcut, colon to underscore (#13740) Signed-off-by: fufesou --- src/platform/windows.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 9481bd69f..bddeb4302 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -1861,13 +1861,17 @@ unsafe fn set_default_dll_directories() -> bool { pub fn create_shortcut(id: &str) -> ResultType<()> { let exe = std::env::current_exe()?.to_str().unwrap_or("").to_owned(); + // https://github.com/rustdesk/rustdesk/issues/13735 + // Replace ':' with '_' for filename since ':' is not allowed in Windows filenames + // https://github.com/rustdesk/hbb_common/blob/8b0e25867375ba9e6bff548acf44fe6d6ffa7c0e/src/config.rs#L1384 + let filename = id.replace(':', "_"); let shortcut = write_cmds( format!( " Set oWS = WScript.CreateObject(\"WScript.Shell\") strDesktop = oWS.SpecialFolders(\"Desktop\") Set objFSO = CreateObject(\"Scripting.FileSystemObject\") -sLinkFile = objFSO.BuildPath(strDesktop, \"{id}.lnk\") +sLinkFile = objFSO.BuildPath(strDesktop, \"{filename}.lnk\") Set oLink = oWS.CreateShortcut(sLinkFile) oLink.TargetPath = \"{exe}\" oLink.Arguments = \"--connect {id}\" From 822b6d1bafc22ce4fa25d3881b200d4f2b6564db Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Tue, 9 Dec 2025 00:07:11 +0800 Subject: [PATCH 021/277] Disable signing commands in flutter-build.yml (#13750) Comment out signing commands in the Flutter build workflow. --- .github/workflows/flutter-build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 4a122bb72..b21c1e342 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -238,7 +238,7 @@ jobs: shell: bash run: | pip3 install requests argparse - BASE_URL=${{ secrets.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./rustdesk/ + # BASE_URL=${{ secrets.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./rustdesk/ - name: Build self-extracted executable shell: bash @@ -269,7 +269,7 @@ jobs: if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '' shell: bash run: | - BASE_URL=${{ secrets.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput + # BASE_URL=${{ secrets.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput - name: Publish Release uses: softprops/action-gh-release@v1 @@ -404,7 +404,7 @@ jobs: shell: bash run: | pip3 install requests argparse - BASE_URL=${{ secrets.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./Release/ + # BASE_URL=${{ secrets.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./Release/ - name: Build self-extracted executable shell: bash @@ -421,7 +421,7 @@ jobs: if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '' shell: bash run: | - BASE_URL=${{ secrets.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput/ + # BASE_URL=${{ secrets.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput/ - name: Publish Release uses: softprops/action-gh-release@v1 From a79776c1c4967647a4efffb84c4666849f275830 Mon Sep 17 00:00:00 2001 From: minh <88567043+MinhAnime@users.noreply.github.com> Date: Tue, 9 Dec 2025 15:58:34 +0700 Subject: [PATCH 022/277] Update Vietnamese translations for various terms (#13756) --- src/lang/vi.rs | 198 ++++++++++++++++++++++--------------------------- 1 file changed, 87 insertions(+), 111 deletions(-) diff --git a/src/lang/vi.rs b/src/lang/vi.rs index d231ec856..26da5ebb7 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -136,7 +136,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ID does not exist", "ID không tồn tại"), ("Failed to connect to rendezvous server", "Không thể kết nối đến máy chủ rendezvous"), ("Please try later", "Thử lại sau"), - ("Remote desktop is offline", "Máy tính từ xa hiện đang offline"), + ("Remote desktop is offline", "Máy tính từ xa hiện đang ngoại tuyến"), ("Key mismatch", "Chìa không khớp"), ("Timeout", "Quá thời gian"), ("Failed to connect to relay server", "Không thể kết nối tới máy chủ chuyển tiếp"), @@ -147,17 +147,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Mật khẩu hệ điều hành"), ("install_tip", "Do UAC, RustDesk sẽ không thể hoạt động đúng cách là bên từ xa trong vài trường hợp. Để tránh UAC, hãy nhấn cái nút dưới đây để cài RustDesk vào hệ thống."), ("Click to upgrade", "Nhấn để nâng cấp"), - ("Configure", "Cài đặt"), + ("Configure", "Cấu hình"), ("config_acc", "Để có thể điều khiển máy tính từ xa, bạn cần phải cung cấp quyền \"Trợ năng\" cho RustDesk"), ("config_screen", "Để có thể truy cập máy tính từ xa, bạn cần phải cung cấp quyền \"Ghi Màn Hình\" cho RustDesk."), - ("Installing ...", "Đang cài ..."), + ("Installing ...", "Đang cài đặt ..."), ("Install", "Cài"), ("Installation", "Cài"), - ("Installation Path", "Địa điểm cài"), + ("Installation Path", "Đường dẫn cài đặt"), ("Create start menu shortcuts", "Tạo shortcut tại start menu"), - ("Create desktop icon", "Tạo biểu tượng trên desktop"), + ("Create desktop icon", "Tạo biểu tượng trên màn hình chính"), ("agreement_tip", "Bằng cách bắt đầu cài đặt, bạn chấp nhận thỏa thuận cấp phép."), - ("Accept and Install", "Chấp nhận và Cài"), + ("Accept and Install", "Chấp nhận và Cài đặtđặt"), ("End-user license agreement", "Thỏa thuận cấp phép dành cho người dùng"), ("Generating ...", "Đang tạo ..."), ("Your installation is lower version.", "Phiên bản của bạn là phiên bản cũ"), @@ -218,18 +218,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Verification code", "Mã xác thực"), ("verification_tip", "Bạn đang đăng nhập trên một thiết bị mới, một mã xác thực đã được gửi tới email đăng ký của bạn, hãy nhập mã xác thực để tiếp tục đăng nhập."), ("Logout", "Đăng xuất"), - ("Tags", "Tags"), + ("Tags", "Thẻ"), ("Search ID", "Tìm ID"), ("whitelist_sep", "Đuợc cách nhau bởi dấu phẩy, dấu chấm phẩy, dấu cách hay dòng mới"), ("Add ID", "Thêm ID"), - ("Add Tag", "Thêm Tag"), - ("Unselect all tags", "Hủy chọn tất cả các tag"), + ("Add Tag", "Thêm thẻ"), + ("Unselect all tags", "Hủy chọn tất cả các thẻ"), ("Network error", "Lỗi mạng"), ("Username missed", "Mất tên người dùng"), ("Password missed", "Mất mật khẩu"), ("Wrong credentials", "Chứng danh bị sai"), - ("The verification code is incorrect or has expired", ""), - ("Edit Tag", "Chỉnh sửa Tag"), + ("The verification code is incorrect or has expired", "Mã xác thực không đúng hoặc đã hết hạn"), + ("Edit Tag", "Chỉnh sửa thẻthẻ"), ("Forget Password", "Quên mật khẩu"), ("Favorites", "Ưa thích"), ("Add to Favorites", "Thêm vào mục Ưa thích"), @@ -507,18 +507,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Start", "Bắt đầu"), ("Stop", "Dừng lại"), ("exceed_max_devices", ""), - ("Sync with recent sessions", ""), + ("Sync with recent sessions", "Đồng bộ với phiên gần đây"), ("Sort tags", ""), - ("Open connection in new tab", ""), + ("Open connection in new tab", "Mở kết nối trong tab mới"), ("Move tab to new window", ""), - ("Can not be empty", ""), + ("Can not be empty", "Không được để trống"), ("Already exists", "Đã tồn tại rồi"), ("Change Password", "Đổi mật khẩu"), - ("Refresh Password", ""), + ("Refresh Password", "Làm mới mật khẩu"), ("ID", ""), - ("Grid View", ""), - ("List View", ""), - ("Select", ""), + ("Grid View", "Xem theo dạng bảng"), + ("List View", "Xem theo dạng danh sách"), + ("Select", "Chọn"), ("Toggle Tags", ""), ("pull_ab_failed_tip", ""), ("push_ab_failed_tip", ""), @@ -539,60 +539,36 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Timeout in minutes", ""), ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", ""), - ("Check for software update on startup", ""), - ("upgrade_rustdesk_server_pro_to_{}_tip", ""), - ("pull_group_failed_tip", ""), - ("Filter by intersection", ""), - ("Remove wallpaper during incoming sessions", ""), - ("Test", ""), - ("display_is_plugged_out_msg", ""), - ("No displays", ""), - ("Open in new window", ""), - ("Show displays as individual windows", ""), - ("Use all my displays for the remote session", ""), - ("selinux_tip", ""), - ("Change view", ""), - ("Big tiles", ""), - ("Small tiles", ""), - ("List", ""), - ("Virtual display", ""), - ("Plug out all", ""), - ("True color (4:4:4)", ""), - ("Enable blocking user input", ""), - ("id_input_tip", ""), - ("privacy_mode_impl_mag_tip", ""), - ("privacy_mode_impl_virtual_display_tip", ""), - ("Enter privacy mode", ""), - ("Exit privacy mode", ""), + ("Check for software update on startupmật"), ("idd_not_support_under_win10_2004_tip", ""), ("input_source_1_tip", ""), ("input_source_2_tip", ""), ("Swap control-command key", ""), ("swap-left-right-mouse", ""), - ("2FA code", ""), - ("More", ""), + ("2FA code", "Mã xác thực 2 bước"), + ("More", "Thêm"), ("enable-2fa-title", ""), ("enable-2fa-desc", ""), ("wrong-2fa-code", ""), ("enter-2fa-title", ""), - ("Email verification code must be 6 characters.", ""), - ("2FA code must be 6 digits.", ""), + ("Email verification code must be 6 characters.", "Mã xác thực email phải có 6 chữ số"), + ("2FA code must be 6 digits.", "Mã xác thực 2 bước phải có 6 chữ số"), ("Multiple Windows sessions found", ""), ("Please select the session you want to connect to", ""), ("powered_by_me", ""), ("outgoing_only_desk_tip", ""), ("preset_password_warning", ""), - ("Security Alert", ""), + ("Security Alert", "Cảnh báo bảo mật"), ("My address book", ""), - ("Personal", ""), - ("Owner", ""), - ("Set shared password", ""), - ("Exist in", ""), - ("Read-only", ""), - ("Read/Write", ""), - ("Full Control", ""), + ("Personal", "Cá nhân"), + ("Owner", "Chủ"), + ("Set shared password", "Cài đặt mật khẩu được chia sẻ"), + ("Exist in", "Tồn tại trong"), + ("Read-only", "Chỉ-đọc"), + ("Read/Write", "Đọc/Ghi"), + ("Full Control", "Toàn quyền"), ("share_warning_tip", ""), - ("Everyone", ""), + ("Everyone", "Mọi người"), ("ab_web_console_tip", ""), ("allow-only-conn-window-open-tip", ""), ("no_need_privacy_mode_no_physical_displays_tip", ""), @@ -610,38 +586,38 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use texture rendering", ""), ("Floating window", ""), ("floating_window_tip", ""), - ("Keep screen on", ""), - ("Never", ""), - ("During controlled", ""), - ("During service is on", ""), - ("Capture screen using DirectX", ""), - ("Back", ""), - ("Apps", ""), - ("Volume up", ""), - ("Volume down", ""), - ("Power", ""), + ("Keep screen on", "Giữ màn hình bật"), + ("Never", "Không bao giờ"), + ("During controlled", "Trong khi được điều khiển"), + ("During service is on", "Trong khi dịch vụ được bật"), + ("Capture screen using DirectX", "Chụp màn hình với DirectX"), + ("Back", "Trở về"), + ("Apps", "Ứng dụng"), + ("Volume up", "Tăng âm lượng"), + ("Volume down", "Giảm âm lượng"), + ("Power", "Nguồn"), ("Telegram bot", ""), ("enable-bot-tip", ""), ("enable-bot-desc", ""), ("cancel-2fa-confirm-tip", ""), ("cancel-bot-confirm-tip", ""), - ("About RustDesk", ""), + ("About RustDesk", "Về RuskDest"), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), - ("Unlock with PIN", ""), + ("Unlock with PIN", "Mở khóa với mã PIN"), ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), + ("Wrong PIN", "Sai mã PIN"), + ("Set PIN", "Đặt mã PIN"), + ("Enable trusted devices", "Kích hoạt thiết bị tin cậy"), + ("Manage trusted devices", "Quản lý thiết bị tin cậy"), + ("Platform", "Nền tảng"), + ("Days remaining", "Số ngày còn lại"), ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), + ("Parent directory", "Thư mục cha"), + ("Resume", "Tiếp tục"), + ("Invalid file name", "Tên tệp không hợp lệ"), ("one-way-file-transfer-tip", ""), - ("Authentication Required", ""), + ("Authentication Required", "Yêu cầu xác thực"), ("Authenticate", ""), ("web_id_input_tip", ""), ("Download", ""), @@ -672,59 +648,59 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", ""), ("save-settings-tip", ""), ("dont-show-again-tip", ""), - ("Take screenshot", ""), - ("Taking screenshot", ""), + ("Take screenshot", "Chụp màn hình"), + ("Taking screenshot", "Đang chụp màn hình"), ("screenshot-merged-screen-not-supported-tip", ""), ("screenshot-action-tip", ""), - ("Save as", ""), - ("Copy to clipboard", ""), - ("Enable remote printer", ""), - ("Downloading {}", ""), + ("Save as", "Lưu thành"), + ("Copy to clipboard", "Sao chép vào bảng nhớ"), + ("Enable remote printer", "Kích hoat máy in ở xa"), + ("Downloading {}", "Đang tải xuống"), ("{} Update", ""), ("{}-to-update-tip", ""), ("download-new-version-failed-tip", ""), - ("Auto update", ""), + ("Auto update", "Tự động cập nhật"), ("update-failed-check-msi-tip", ""), ("websocket_tip", ""), - ("Use WebSocket", ""), - ("Trackpad speed", ""), - ("Default trackpad speed", ""), - ("Numeric one-time password", ""), - ("Enable IPv6 P2P connection", ""), + ("Use WebSocket", "Sử dụng WebSocket"), + ("Trackpad speed", "Tốc độ trackpad"), + ("Default trackpad speed", "Tốc độ trackpad mặc định"), + ("Numeric one-time password", "Mật khẩu số dùng một lần"), + ("Enable IPv6 P2P connection", "Cho phép kết nốt IPv6 P2P"), ("Enable UDP hole punching", ""), ("View camera", "Xem camera"), - ("Enable camera", ""), - ("No cameras", ""), + ("Enable camera", "Kích hoạt máy ảnh"), + ("No cameras", "Không có máy ảnh"), ("view_camera_unsupported_tip", ""), - ("Terminal", ""), - ("Enable terminal", ""), - ("New tab", ""), - ("Keep terminal sessions on disconnect", ""), - ("Terminal (Run as administrator)", ""), + ("Terminal", "Bảng điều khiển"), + ("Enable terminal", "Kích hoạt bảng điều khiển"), + ("New tab", "Tab mới"), + ("Keep terminal sessions on disconnect", "Giữ các phiên của bảng điều khiển ngắt kết nối"), + ("Terminal (Run as administrator)", "Bảng điều khiển (Chạy với quyền quản trị viên)"), ("terminal-admin-login-tip", ""), - ("Failed to get user token.", ""), - ("Incorrect username or password.", ""), - ("The user is not an administrator.", ""), - ("Failed to check if the user is an administrator.", ""), - ("Supported only in the installed version.", ""), + ("Failed to get user token.", "Thất bại trong việc lấy token của người dùng"), + ("Incorrect username or password.", "Tên người dùng hoặc mật khẩu không chính xác."), + ("The user is not an administrator.", "Người dùng không phải là quản trị viên."), + ("Failed to check if the user is an administrator.", "Thất bại trong việc kiểm tra người dùng là quản trị viên."), + ("Supported only in the installed version.", "Chỉ hỗ trợ phiên bản đã được cài đặt."), ("elevation_username_tip", ""), - ("Preparing for installation ...", ""), - ("Show my cursor", ""), - ("Scale custom", ""), + ("Preparing for installation ...", "Đang chuẩn bị để cài đặt ..."), + ("Show my cursor", "Hiện con trỏ"), + ("Scale custom", "Tùy chỉnh "), ("Custom scale slider", ""), - ("Decrease", ""), - ("Increase", ""), - ("Show virtual mouse", ""), - ("Virtual mouse size", ""), + ("Decrease", "Giảm"), + ("Increase", "Tăng"), + ("Show virtual mouse", "Hiện chuột ảo"), + ("Virtual mouse size", "Kích thước chuột ảo"), ("Small", "Nhỏ"), ("Large", "Lớn"), - ("Show virtual joystick", ""), + ("Show virtual joystick", "Hiện nút điều khiển ảo"), ("Edit note", "Sửa ghi chép"), ("Alias", "Ánh xạ"), ("ScrollEdge", ""), ("Allow insecure TLS fallback", ""), ("allow-insecure-tls-fallback-tip", ""), - ("Disable UDP", ""), + ("Disable UDP", "Ngắt kết nối UDP"), ("disable-udp-tip", ""), ("server-oss-not-support-tip", ""), ("input note here", ""), From a0537759b13686bdb30fbbdbe644de039f38eb53 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 10 Dec 2025 00:31:13 +0800 Subject: [PATCH 023/277] fix vi --- src/lang/vi.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/vi.rs b/src/lang/vi.rs index 26da5ebb7..0f3ae4fec 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -539,7 +539,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Timeout in minutes", ""), ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", ""), - ("Check for software update on startupmật"), + ("Check for software update on startupmật", ""), ("idd_not_support_under_win10_2004_tip", ""), ("input_source_1_tip", ""), ("input_source_2_tip", ""), From 735862d1fd0082355ee8fb5f3be6158f4cd05f80 Mon Sep 17 00:00:00 2001 From: YuZhiYuanDev <203504060+YuZhiYuanDev@users.noreply.github.com> Date: Wed, 10 Dec 2025 17:05:52 +0800 Subject: [PATCH 024/277] Replace unsupported macos-13 with a new runner (#13767) --- .github/workflows/flutter-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index b21c1e342..83fb3b786 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -562,7 +562,7 @@ jobs: job: - { target: x86_64-apple-darwin, - os: macos-13, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel + os: macos-15-intel, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel extra-build-args: "", arch: x86_64, vcpkg-triplet: x64-osx, From de9d86621d36f88f0284d1e6d5d1650ea79dd961 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 11 Dec 2025 15:39:18 +0800 Subject: [PATCH 025/277] fix: macos, clipboard, text-based items (#13778) Signed-off-by: fufesou --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 33ba832d2..e3f95bc26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -286,7 +286,7 @@ checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "arboard" version = "3.4.0" -source = "git+https://github.com/rustdesk-org/arboard#4e16bad260ea05dd7dcdb68cc7549dad3920b940" +source = "git+https://github.com/rustdesk-org/arboard#85be1218668ff218a7b170c9d424fde73e069914" dependencies = [ "clipboard-win", "core-graphics 0.23.2", From 0112b3387ed8a536a5f11c45ec3f3b1cec0e9338 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 11 Dec 2025 20:16:06 +0800 Subject: [PATCH 026/277] fix(CI): macOS, nasm, use 2.16.x (#13781) Signed-off-by: fufesou --- .github/workflows/flutter-build.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 83fb3b786..49b5d4b5c 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -623,7 +623,7 @@ jobs: - name: Install build runtime run: | - brew install llvm create-dmg nasm + brew install llvm create-dmg # pkg-config is handled in a separate step, because it may be already installed by `macos-latest`(14.7.1) runner if command -v pkg-config &>/dev/null; then echo "pkg-config is already installed" @@ -631,6 +631,17 @@ jobs: brew install pkg-config fi + - name: Install NASM + run: | + # Install NASM 2.16.x from official release. + # Do NOT use `brew install nasm` which installs NASM 3.x. + # NASM 3.x is a complete rewrite with incompatible CLI options and removed features. + # aom and other multimedia libraries require NASM 2.x for x86/x86_64 assembly. + wget https://www.nasm.us/pub/nasm/releasebuilds/2.16.03/macosx/nasm-2.16.03-macosx.zip + unzip nasm-2.16.03-macosx.zip + sudo cp nasm-2.16.03/nasm /usr/local/bin/nasm + nasm --version + - name: Install flutter uses: subosito/flutter-action@v2 with: From b9a1369c6f4530c3a4d9181469ab88fc12f517df Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 11 Dec 2025 21:17:42 +0800 Subject: [PATCH 027/277] fix: custom client, contains RustDesk (#13783) Signed-off-by: fufesou --- src/lang.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/lang.rs b/src/lang.rs index 13734d60a..4c49c48ca 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -186,7 +186,26 @@ pub fn translate_locale(name: String, locale: &str) -> String { && !name.starts_with("upgrade_rustdesk_server_pro") && name != "powered_by_me" { - s = s.replace("RustDesk", &crate::get_app_name()); + let app_name = crate::get_app_name(); + if !app_name.contains("RustDesk") { + s = s.replace("RustDesk", &app_name); + } else { + // https://github.com/rustdesk/rustdesk-server-pro/issues/845 + // If app_name contains "RustDesk" (e.g., "RustDesk-Admin"), we need to avoid + // replacing "RustDesk" within the already-substituted app_name, which would + // cause duplication like "RustDesk-Admin" -> "RustDesk-Admin-Admin". + // + // app_name only contains alphanumeric and hyphen. + const PLACEHOLDER: &str = "#A-P-P-N-A-M-E#"; + if !s.contains(PLACEHOLDER) { + s = s.replace(&app_name, PLACEHOLDER); + s = s.replace("RustDesk", &app_name); + s = s.replace(PLACEHOLDER, &app_name); + } else { + // It's very unlikely to reach here. + // Skip replacement to avoid incorrect result. + } + } } } s From 7bdfa121f39c6f2aeee33da7f248db59fd605953 Mon Sep 17 00:00:00 2001 From: Mahdi Rahimi <31624047+mahdirahimi1999@users.noreply.github.com> Date: Fri, 12 Dec 2025 17:07:15 +0330 Subject: [PATCH 028/277] Update Arabic translation in ar.rs (#13738) --- src/lang/ar.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 60f5ac2f6..3e5b9ce2d 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -719,15 +719,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Small", "صغير"), ("Large", "كبير"), ("Show virtual joystick", "إظهار عصا التحكم الافتراضية"), - ("Edit note", ""), - ("Alias", ""), - ("ScrollEdge", ""), - ("Allow insecure TLS fallback", ""), - ("allow-insecure-tls-fallback-tip", ""), - ("Disable UDP", ""), - ("disable-udp-tip", ""), - ("server-oss-not-support-tip", ""), - ("input note here", ""), - ("note-at-conn-end-tip", ""), + ("Edit note", "تعديل الملاحظة"), + ("Alias", "اسم مستعار"), + ("ScrollEdge", "حافة التمرير"), + ("Allow insecure TLS fallback", "السماح بالرجوع إلى TLS غير الآمن"), + ("allow-insecure-tls-fallback-tip", "يسمح باستخدام اتصال TLS غير آمن عند فشل الاتصال الآمن"), + ("Disable UDP", "تعطيل UDP"), + ("disable-udp-tip", "عند التفعيل لن يتم استخدام بروتوكول UDP"), + ("server-oss-not-support-tip", "هذه الميزة غير مدعومة من قبل خادمك"), + ("input note here", "أدخل الملاحظة هنا"), + ("note-at-conn-end-tip", "سيتم عرض هذه الملاحظة عند نهاية الاتصال"), ].iter().cloned().collect(); } From da2c678fb32e78816c75ca60f9502750d95388ab Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Sun, 14 Dec 2025 17:41:18 +0800 Subject: [PATCH 029/277] Revert "Disable signing commands in flutter-build.yml (#13750)" (#13808) This reverts commit 822b6d1bafc22ce4fa25d3881b200d4f2b6564db. --- .github/workflows/flutter-build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 49b5d4b5c..8e549ce05 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -238,7 +238,7 @@ jobs: shell: bash run: | pip3 install requests argparse - # BASE_URL=${{ secrets.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./rustdesk/ + BASE_URL=${{ secrets.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./rustdesk/ - name: Build self-extracted executable shell: bash @@ -269,7 +269,7 @@ jobs: if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '' shell: bash run: | - # BASE_URL=${{ secrets.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput + BASE_URL=${{ secrets.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput - name: Publish Release uses: softprops/action-gh-release@v1 @@ -404,7 +404,7 @@ jobs: shell: bash run: | pip3 install requests argparse - # BASE_URL=${{ secrets.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./Release/ + BASE_URL=${{ secrets.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./Release/ - name: Build self-extracted executable shell: bash @@ -421,7 +421,7 @@ jobs: if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '' shell: bash run: | - # BASE_URL=${{ secrets.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput/ + BASE_URL=${{ secrets.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput/ - name: Publish Release uses: softprops/action-gh-release@v1 From a32d36a97be9fab7001382e5f0992f373b0d61f5 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sun, 14 Dec 2025 20:52:10 +0800 Subject: [PATCH 030/277] fix(sudo -E): Ubuntu 25.10, run_as_user (#13796) Signed-off-by: fufesou --- src/platform/linux.rs | 295 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 280 insertions(+), 15 deletions(-) diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 66eefb8a2..569c20f9f 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -14,7 +14,8 @@ use hbb_common::{ }; use std::{ cell::RefCell, - ffi::OsStr, + ffi::{OsStr, OsString}, + os::unix::ffi::OsStrExt, path::{Path, PathBuf}, process::{Child, Command}, string::String, @@ -47,6 +48,36 @@ lazy_static::lazy_static! { } } }; + // https://github.com/rustdesk/rustdesk/issues/13705 + // Check if `sudo -E` actually preserves environment. + // + // This flag is only used by `run_as_user()` (root service -> user session). If the current process is not + // running as `root`, this check is meaningless (and `sudo -n` may fail), so we return `false` directly. + // + // On Ubuntu 25.10, `sudo -E` may still succeed but effectively ignores `-E`. Some versions print a warning + // to stderr (wording may vary by locale), so we verify behavior instead: + // - Inject a sentinel environment variable into the `sudo` process + // - Run `sudo -n -E env` and check whether the sentinel is present in stdout + static ref SUDO_E_PRESERVES_ENV: bool = { + if !is_root() { + log::warn!("Not running as root, SUDO_E_PRESERVES_ENV check skipped"); + false + } else { + let key = format!("__RUSTDESK_SUDO_E_TEST_{}", std::process::id()); + let val = "1"; + let expected = format!("{key}={val}"); + Command::new("sudo") + // -n for non-interactive to avoid password prompt + .env(&key, val) + .args(["-n", "-E", "env"]) + .output() + .map(|o| { + o.status.success() + && String::from_utf8_lossy(&o.stdout).contains(expected.as_str()) + }) + .unwrap_or(false) + } + }; } thread_local! { @@ -773,14 +804,58 @@ where if uid.is_empty() { bail!("No valid uid"); } - let xdg = &format!("XDG_RUNTIME_DIR=/run/user/{}", uid) as &str; - let mut args = vec![xdg, "-u", &username, cmd.to_str().unwrap_or("")]; - args.append(&mut arg.clone()); - // -E is required to preserve env - args.insert(0, "-E"); - let task = Command::new("sudo").envs(envs).args(args).spawn()?; - Ok(Some(task)) + let xdg = &format!("XDG_RUNTIME_DIR=/run/user/{uid}"); + if *SUDO_E_PRESERVES_ENV { + // Original logic: use sudo -E to preserve environment + let mut args = vec![xdg, "-u", &username, cmd.to_str().unwrap_or("")]; + args.append(&mut arg.clone()); + // -E is required to preserve env + args.insert(0, "-E"); + let task = Command::new("sudo").envs(envs).args(args).spawn()?; + Ok(Some(task)) + } else { + // Fallback: sudo -u username env VAR=VALUE ... cmd args + // For systems where sudo -E is not supported (e.g., Ubuntu 25.10+) + // + // SECURITY: No shell is involved here (we use execve-style argv). + // Environment is passed via `env` arguments, + // so there is no shell injection vector. + // + // Only accept portable env var names (POSIX portable character set for shells). + // Most legitimate env vars follow [A-Za-z_][A-Za-z0-9_]* convention. + // Variables with dots (e.g., "java.home") are Java system properties, not env vars. + // Being restrictive here is intentional for security in this sudo context. + fn is_valid_env_key(key: &str) -> bool { + let mut it = key.chars(); + match it.next() { + Some(c) if c.is_ascii_alphabetic() || c == '_' => {} + _ => return false, + } + it.all(|c| c.is_ascii_alphanumeric() || c == '_') + } + + let mut sudo = Command::new("sudo"); + sudo.arg("-u").arg(&username).arg("--").arg("env").arg(xdg); + + for (k, v) in envs { + let key = k.as_ref().to_string_lossy(); + if !is_valid_env_key(&key) { + log::warn!("Skipping environment variable with invalid key: '{}'. Only [A-Za-z_][A-Za-z0-9_]* are allowed in sudo context.", key); + continue; + } + // IMPORTANT: do NOT add shell quotes here; `Command` does not invoke a shell. + // Passing KEY=VALUE as a single argv element is safe and preserves spaces. + let mut arg = OsString::from(&*key); + arg.push("="); + arg.push(v.as_ref()); + sudo.arg(arg); + } + + sudo.arg(cmd).args(arg); + let task = sudo.spawn()?; + Ok(Some(task)) + } } pub fn get_pa_monitor() -> String { @@ -861,6 +936,156 @@ pub fn is_installed() -> bool { } } +/// Get multiple environment variables from a process matching the given criteria. +/// This version reads /proc directly instead of spawning shell commands. +/// +/// # Arguments +/// * `uid` - User ID to filter processes +/// * `process_pat` - Regex pattern to match process cmdline +/// * `names` - Environment variable names to retrieve. **Must be <= 64 elements** due to +/// the internal bitmask used for tie-breaking. +/// +/// # Panics (debug builds) +/// Panics if `names.len() > 64`. +/// +/// # Implementation notes +/// - Returns values from a *single* best-matching process_pat (for consistency). +/// - Avoids repeated scanning by parsing `environ` once per process. +fn get_envs<'a>( + uid: &str, + process_pat: &str, + names: &[&'a str], +) -> std::collections::HashMap<&'a str, String> { + // The tie-breaking logic uses a u64 bitmask, limiting us to 64 variables. + debug_assert!( + names.len() <= 64, + "get_envs: names.len() must be <= 64, got {}", + names.len() + ); + + let empty: std::collections::HashMap<&'a str, String> = + names.iter().map(|&n| (n, String::new())).collect(); + + let Ok(uid_num) = uid.parse::() else { + return empty; + }; + let Ok(re) = Regex::new(process_pat) else { + return empty; + }; + + // Used for stable tie-breaking when multiple processes match. + // Higher bits correspond to earlier entries in `names`. + let name_indices: std::collections::HashMap<&'a str, usize> = + names.iter().enumerate().map(|(i, &n)| (n, i)).collect(); + + let mut best = empty.clone(); + let mut best_count = 0usize; + let mut best_mask: u64 = 0; + + // Iterate /proc to find matching processes + let Ok(entries) = std::fs::read_dir("/proc") else { + return best; + }; + + for entry in entries.flatten() { + let file_name = entry.file_name(); + let Some(pid_str) = file_name.to_str() else { + continue; + }; + if !pid_str.chars().all(|c| c.is_ascii_digit()) { + continue; + } + + let proc_path = entry.path(); + + // Check if process belongs to the specified uid + if let Ok(meta) = std::fs::metadata(&proc_path) { + use std::os::unix::fs::MetadataExt; + if meta.uid() != uid_num { + continue; + } + } else { + continue; + } + + // Check cmdline matches process pattern + let cmdline_path = proc_path.join("cmdline"); + let Ok(cmdline) = std::fs::read(&cmdline_path) else { + continue; + }; + let cmdline_str = String::from_utf8_lossy(&cmdline).replace('\0', " "); + if !re.is_match(&cmdline_str) { + continue; + } + + // Read environ and extract matching variables + let environ_path = proc_path.join("environ"); + let Ok(environ) = std::fs::read(&environ_path) else { + continue; + }; + + let mut found = empty.clone(); + let mut found_count = 0usize; + let mut found_mask: u64 = 0; + + for part in environ.split(|&b| b == 0) { + if part.is_empty() { + continue; + } + let Some(eq) = part.iter().position(|&b| b == b'=') else { + continue; + }; + let key_bytes = &part[..eq]; + let val_bytes = &part[eq + 1..]; + + let Ok(key) = std::str::from_utf8(key_bytes) else { + continue; + }; + if let Some(slot) = found.get_mut(key) { + if slot.is_empty() { + *slot = String::from_utf8_lossy(val_bytes).into_owned(); + found_count += 1; + + if let Some(&idx) = name_indices.get(key) { + let total = names.len(); + if total <= 64 { + let bit = 1u64 << (total - 1 - idx); + found_mask |= bit; + } + } + + if found_count == names.len() { + return found; + } + } + } + } + + if found_count > best_count || (found_count == best_count && found_mask > best_mask) { + best = found; + best_count = found_count; + best_mask = found_mask; + } + } + + best +} + +/// Deprecated: Use `get_envs` instead. +/// +/// https://github.com/rustdesk/rustdesk/discussions/11959 +/// +/// **Note**: This function is retained for conservative migration. The plan is to gradually +/// transition all callers to `get_envs` after it proves stable and reliable. Once `get_envs` +/// is confirmed to work correctly across all use cases, this function will be removed entirely. +/// +/// # Arguments +/// * `name` - Environment variable name to retrieve +/// * `uid` - User ID to filter processes +/// * `process` - Process name pattern to match +/// +/// # Returns +/// The environment variable value, or empty string if not found #[inline] fn get_env(name: &str, uid: &str, process: &str) -> String { let cmd = format!("ps -u {} -f | grep -E '{}' | grep -v 'grep' | tail -1 | awk '{{print $2}}' | xargs -I__ cat /proc/__/environ 2>/dev/null | tr '\\0' '\\n' | grep '^{}=' | tail -1 | sed 's/{}=//g'", uid, process, name, name); @@ -1100,11 +1325,18 @@ mod desktop { pub const XFCE4_PANEL: &str = "xfce4-panel"; pub const SDDM_GREETER: &str = "sddm-greeter"; + // xdg-desktop-portal runs on all Wayland desktops (GNOME, KDE, wlroots, etc.) + const XDG_DESKTOP_PORTAL: &str = "xdg-desktop-portal"; const XWAYLAND: &str = "Xwayland"; const IBUS_DAEMON: &str = "ibus-daemon"; const PLASMA_KDED: &str = "kded[0-9]+"; const GNOME_GOA_DAEMON: &str = "goa-daemon"; + const ENV_KEY_DISPLAY: &str = "DISPLAY"; + const ENV_KEY_XAUTHORITY: &str = "XAUTHORITY"; + const ENV_KEY_WAYLAND_DISPLAY: &str = "WAYLAND_DISPLAY"; + const ENV_KEY_DBUS_SESSION_BUS_ADDRESS: &str = "DBUS_SESSION_BUS_ADDRESS"; + #[derive(Debug, Clone, Default)] pub struct Desktop { pub sid: String, @@ -1135,10 +1367,42 @@ mod desktop { self.sid.is_empty() || self.is_rustdesk_subprocess } + fn get_display_xauth_wayland(&mut self) { + for _ in 1..=10 { + // Prefer Wayland-related variables first when multiple portal processes match. + let mut envs = get_envs( + &self.uid, + XDG_DESKTOP_PORTAL, + &[ + ENV_KEY_WAYLAND_DISPLAY, + ENV_KEY_DBUS_SESSION_BUS_ADDRESS, + ENV_KEY_DISPLAY, + ENV_KEY_XAUTHORITY, + ], + ); + self.display = envs.remove(ENV_KEY_DISPLAY).unwrap_or_default(); + self.xauth = envs.remove(ENV_KEY_XAUTHORITY).unwrap_or_default(); + self.wl_display = envs.remove(ENV_KEY_WAYLAND_DISPLAY).unwrap_or_default(); + self.dbus = envs + .remove(ENV_KEY_DBUS_SESSION_BUS_ADDRESS) + .unwrap_or_default(); + // For pure Wayland sessions, prefer `WAYLAND_DISPLAY`. + // NOTE: On some systems (e.g. Ubuntu 25.10), `DISPLAY`/`XAUTHORITY` may exist even when XWayland + // is not running, so do NOT treat them as a success condition here. + let has_wayland = !self.wl_display.is_empty(); + let has_dbus = !self.dbus.is_empty(); + if has_wayland && has_dbus { + return; + } + sleep_millis(300); + } + } + fn get_display_xauth_xwayland(&mut self) { let tray = format!("{} +--tray", crate::get_app_name().to_lowercase()); for _ in 1..=10 { let display_proc = vec![ + XDG_DESKTOP_PORTAL, XWAYLAND, IBUS_DAEMON, GNOME_GOA_DAEMON, @@ -1146,10 +1410,10 @@ mod desktop { tray.as_str(), ]; for proc in display_proc { - self.display = get_env("DISPLAY", &self.uid, proc); - self.xauth = get_env("XAUTHORITY", &self.uid, proc); - self.wl_display = get_env("WAYLAND_DISPLAY", &self.uid, proc); - self.dbus = get_env("DBUS_SESSION_BUS_ADDRESS", &self.uid, proc); + self.display = get_env(ENV_KEY_DISPLAY, &self.uid, proc); + self.xauth = get_env(ENV_KEY_XAUTHORITY, &self.uid, proc); + self.wl_display = get_env(ENV_KEY_WAYLAND_DISPLAY, &self.uid, proc); + self.dbus = get_env(ENV_KEY_DBUS_SESSION_BUS_ADDRESS, &self.uid, proc); if !self.display.is_empty() && !self.xauth.is_empty() { return; } @@ -1169,7 +1433,7 @@ mod desktop { SDDM_GREETER, ]; for proc in display_proc { - self.display = get_env("DISPLAY", &self.uid, proc); + self.display = get_env(ENV_KEY_DISPLAY, &self.uid, proc); if !self.display.is_empty() { break; } @@ -1359,6 +1623,8 @@ mod desktop { if is_xwayland_running() && !self.is_login_wayland() { self.get_display_xauth_xwayland(); self.is_rustdesk_subprocess = false; + } else if self.is_wayland() { + self.get_display_xauth_wayland(); } return; } @@ -1386,8 +1652,7 @@ mod desktop { if is_xwayland_running() { self.get_display_xauth_xwayland(); } else { - self.display = "".to_owned(); - self.xauth = "".to_owned(); + self.get_display_xauth_wayland(); } self.is_rustdesk_subprocess = false; } else { From e4faedcb62136fe0a4cf37e37864867d708b1a20 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Mon, 15 Dec 2025 19:51:48 +0800 Subject: [PATCH 031/277] Update flutter-build.yml (#13815) --- .github/workflows/flutter-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 8e549ce05..1ef91738a 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -45,7 +45,7 @@ env: ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}" UPLOAD_ARTIFACT: "${{ inputs.upload-artifact }}" - SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}" + SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}-2" jobs: generate-bridge: From 692e90f7799e1a5e55fb5a19c9b98b9e4bba0d61 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Tue, 16 Dec 2025 10:33:50 +0800 Subject: [PATCH 032/277] Update flutter-build.yml (#13817) --- .github/workflows/flutter-build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 1ef91738a..fa2a622a0 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -238,7 +238,7 @@ jobs: shell: bash run: | pip3 install requests argparse - BASE_URL=${{ secrets.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./rustdesk/ + BASE_URL=${{ env.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./rustdesk/ - name: Build self-extracted executable shell: bash @@ -269,7 +269,7 @@ jobs: if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '' shell: bash run: | - BASE_URL=${{ secrets.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput + BASE_URL=${{ env.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput - name: Publish Release uses: softprops/action-gh-release@v1 @@ -404,7 +404,7 @@ jobs: shell: bash run: | pip3 install requests argparse - BASE_URL=${{ secrets.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./Release/ + BASE_URL=${{ env.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./Release/ - name: Build self-extracted executable shell: bash @@ -421,7 +421,7 @@ jobs: if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '' shell: bash run: | - BASE_URL=${{ secrets.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput/ + BASE_URL=${{ env.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput/ - name: Publish Release uses: softprops/action-gh-release@v1 From 3e0688ab6318ca9e8f0837886557f164debf4922 Mon Sep 17 00:00:00 2001 From: mehdi-song Date: Wed, 17 Dec 2025 18:02:16 +0330 Subject: [PATCH 033/277] Update fa.rs (#13818) * Update fa.rs :-) * Update fa.rs --- src/lang/fa.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/lang/fa.rs b/src/lang/fa.rs index f51a76860..0b5a3eafa 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -695,19 +695,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View camera", "نمایش دوربین"), ("Enable camera", "فعال کردن دوربین"), ("No cameras", "هیچ دوربینی یافت نشد"), - ("view_camera_unsupported_tip", "دوربین در این دستگاه پشتیبانی نمی‌شود"), + ("view_camera_unsupported_tip", "ریموت از مشاهده دوربین پشتیبانی نمی کند."), ("Terminal", "ترمینال"), ("Enable terminal", "فعال‌سازی ترمینال"), ("New tab", "زبانه جدید"), ("Keep terminal sessions on disconnect", "حفظ جلسات ترمینال پس از قطع اتصال"), ("Terminal (Run as administrator)", "ترمینال (اجرای به عنوان مدیر سیستم)"), - ("terminal-admin-login-tip", "برای اجرای ترمینال به‌عنوان مدیر، نام کاربری و رمز عبور مدیر سیستم را وارد کنید."), + ("terminal-admin-login-tip", "برای اجرای ترمینال به‌ عنوان مدیر، نام کاربری و رمز عبور مدیر سیستم ریموت را وارد کنید."), ("Failed to get user token.", "دریافت توکن کاربر ناموفق بود."), ("Incorrect username or password.", "نام کاربری یا رمز عبور اشتباه است."), ("The user is not an administrator.", "کاربر دارای دسترسی مدیر سیستم نیست."), ("Failed to check if the user is an administrator.", "بررسی وضعیت مدیر سیستم برای کاربر ناموفق بود."), - ("Supported only in the installed version.", "فقط در نسخه نصب‌شده پشتیبانی می‌شود."), - ("elevation_username_tip", "لطفاً نام کاربری مدیریتی را برای ارتقاء دسترسی وارد کنید."), + ("Supported only in the installed version.", "فقط در نسخه نصب‌ شده پشتیبانی می‌شود."), + ("elevation_username_tip", "وارد نمایید domain\\username یا username نام کاربری را به صورت"), ("Preparing for installation ...", "در حال آماده‌سازی برای نصب..."), ("Show my cursor", "نمایش نشانگر من"), ("Scale custom", "مقیاس سفارشی"), @@ -719,15 +719,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Small", "کوچک"), ("Large", "بزرگ"), ("Show virtual joystick", "نمایش جوی‌استیک مجازی"), - ("Edit note", ""), - ("Alias", ""), + ("Edit note", "ویرایش یادداشت"), + ("Alias", "نام مستعار"), ("ScrollEdge", ""), - ("Allow insecure TLS fallback", ""), - ("allow-insecure-tls-fallback-tip", ""), - ("Disable UDP", ""), - ("disable-udp-tip", ""), - ("server-oss-not-support-tip", ""), - ("input note here", ""), - ("note-at-conn-end-tip", ""), + ("Allow insecure TLS fallback", "استفاده از TLS غیر امن در ارتباط"), + ("allow-insecure-tls-fallback-tip", "به‌طور پیش‌فرض، RustDesk گواهی سرور را برای پروتکل‌ها با استفاده از TLS تأیید می‌کند.\nبا فعال بودن این گزینه، RustDesk دوباره مرحله تأیید را رد می‌کند و در صورت عدم موفقیت تأیید ادامه می‌دهد."), + ("Disable UDP", "UDP غیر فعال کردن"), + ("disable-udp-tip", "کنترل می کند که آیا فقط از TCP استفاده شود یا خیر.\nوقتی این گزینه فعال باشد، RustDesk دیگر از UDP 21116 استفاده نمی کند، به جای آن از TCP 21116 استفاده می شود."), + ("server-oss-not-support-tip", "توجه: سرور RustDesk OSS این ویژگی را ندارد."), + ("input note here", "یادداشت را اینجا وارد کنید"), + ("note-at-conn-end-tip", "در پایان اتصال، یادداشت بخواهید"), ].iter().cloned().collect(); } From d6463f95b9830a4e2979c956da7f78146aa98513 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 19 Dec 2025 20:45:22 +0800 Subject: [PATCH 034/277] refact: remote toolbar show/hide (#13843) Signed-off-by: fufesou --- flutter/lib/consts.dart | 1 + flutter/lib/desktop/pages/remote_page.dart | 1 - .../lib/desktop/pages/remote_tab_page.dart | 4 +- .../lib/desktop/pages/view_camera_page.dart | 1 - .../desktop/pages/view_camera_tab_page.dart | 4 +- .../lib/desktop/widgets/remote_toolbar.dart | 99 +++++++++++++------ 6 files changed, 73 insertions(+), 37 deletions(-) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index cf91e14d2..6c68d3d91 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -120,6 +120,7 @@ const String kOptionApproveMode = "approve-mode"; const String kOptionAllowNumericOneTimePassword = "allow-numeric-one-time-password"; const String kOptionCollapseToolbar = "collapse_toolbar"; +const String kOptionHideToolbar = "hide-toolbar"; const String kOptionShowRemoteCursor = "show_remote_cursor"; const String kOptionFollowRemoteCursor = "follow_remote_cursor"; const String kOptionFollowRemoteWindow = "follow_remote_window"; diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index e31196dc8..a752efe6b 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -509,7 +509,6 @@ class _RemotePageState extends State () => _ffi.ffiModel.pi.isSet.isFalse ? Container(color: Colors.transparent) : Obx(() { - widget.toolbarState.initShow(sessionId); _ffi.textureModel.updateCurrentDisplay(peerDisplay.value); return ImagePaint( id: widget.id, diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 6a9f1e89d..af285ac35 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -251,11 +251,11 @@ class _ConnectionTabPageState extends State { MenuEntryButton( childBuilder: (TextStyle? style) => Obx(() => Text( translate( - toolbarState.show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'), + toolbarState.hide.isTrue ? 'Show Toolbar' : 'Hide Toolbar'), style: style, )), proc: () { - toolbarState.switchShow(sessionId); + toolbarState.switchHide(sessionId); cancelFunc(); }, padding: padding, diff --git a/flutter/lib/desktop/pages/view_camera_page.dart b/flutter/lib/desktop/pages/view_camera_page.dart index 4be6fdc57..6be074b59 100644 --- a/flutter/lib/desktop/pages/view_camera_page.dart +++ b/flutter/lib/desktop/pages/view_camera_page.dart @@ -465,7 +465,6 @@ class _ViewCameraPageState extends State () => _ffi.ffiModel.pi.isSet.isFalse ? Container(color: Colors.transparent) : Obx(() { - widget.toolbarState.initShow(sessionId); _ffi.textureModel.updateCurrentDisplay(peerDisplay.value); return ImagePaint( id: widget.id, diff --git a/flutter/lib/desktop/pages/view_camera_tab_page.dart b/flutter/lib/desktop/pages/view_camera_tab_page.dart index 4c04cb8b8..36fa623ff 100644 --- a/flutter/lib/desktop/pages/view_camera_tab_page.dart +++ b/flutter/lib/desktop/pages/view_camera_tab_page.dart @@ -250,11 +250,11 @@ class _ViewCameraTabPageState extends State { MenuEntryButton( childBuilder: (TextStyle? style) => Obx(() => Text( translate( - toolbarState.show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'), + toolbarState.hide.isTrue ? 'Show Toolbar' : 'Hide Toolbar'), style: style, )), proc: () { - toolbarState.switchShow(sessionId); + toolbarState.switchHide(sessionId); cancelFunc(); }, padding: padding, diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index bc3757f1e..06675f9ec 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -31,8 +31,12 @@ import 'package:flutter_hbb/common/widgets/custom_scale_base.dart'; class ToolbarState { late RxBool _pin; - bool isShowInited = false; - RxBool show = false.obs; + RxBool collapse = false.obs; + RxBool hide = false.obs; + + // Track initialization state to prevent flickering + final RxBool initialized = false.obs; + bool _isInitializing = false; ToolbarState() { _pin = RxBool(false); @@ -53,19 +57,39 @@ class ToolbarState { bool get pin => _pin.value; - switchShow(SessionID sessionId) async { - bind.sessionToggleOption( - sessionId: sessionId, value: kOptionCollapseToolbar); - show.value = !show.value; + /// Initialize all toolbar states from session options. + /// This should be called once when the toolbar is first created. + Future init(SessionID sessionId) async { + if (initialized.value || _isInitializing) return; + _isInitializing = true; + + try { + // Load both states in parallel for better performance + final results = await Future.wait([ + bind.sessionGetToggleOption( + sessionId: sessionId, arg: kOptionCollapseToolbar), + bind.sessionGetToggleOption( + sessionId: sessionId, arg: kOptionHideToolbar), + ]); + + collapse.value = results[0] ?? false; + hide.value = results[1] ?? false; + } finally { + _isInitializing = false; + initialized.value = true; + } } - initShow(SessionID sessionId) async { - if (!isShowInited) { - show.value = !(await bind.sessionGetToggleOption( - sessionId: sessionId, arg: kOptionCollapseToolbar) ?? - false); - isShowInited = true; - } + switchCollapse(SessionID sessionId) async { + bind.sessionToggleOption( + sessionId: sessionId, value: kOptionCollapseToolbar); + collapse.value = !collapse.value; + } + + // Switch hide state for entire toolbar visibility + switchHide(SessionID sessionId) async { + bind.sessionToggleOption(sessionId: sessionId, value: kOptionHideToolbar); + hide.value = !hide.value; } switchPin() async { @@ -237,7 +261,8 @@ class _RemoteToolbarState extends State { // setState(() {}); } - RxBool get show => widget.state.show; + RxBool get collapse => widget.state.collapse; + RxBool get hide => widget.state.hide; bool get pin => widget.state.pin; PeerInfo get pi => widget.ffi.ffiModel.pi; @@ -258,6 +283,8 @@ class _RemoteToolbarState extends State { arg: 'remote-menubar-drag-x') ?? '0.5') ?? 0.5; + // Initialize toolbar states (collapse, hide) from session options + widget.state.init(widget.ffi.sessionId); }); _debouncerHide = Debouncer( @@ -277,8 +304,8 @@ class _RemoteToolbarState extends State { } _debouncerHideProc(int v) { - if (!pin && show.isTrue && _isCursorOverImage && _dragging.isFalse) { - show.value = false; + if (!pin && collapse.isFalse && _isCursorOverImage && _dragging.isFalse) { + collapse.value = true; } } @@ -291,17 +318,27 @@ class _RemoteToolbarState extends State { @override Widget build(BuildContext context) { - return Align( - alignment: Alignment.topCenter, - child: Obx(() => show.value - ? _buildToolbar(context) - : _buildDraggableShowHide(context)), - ); + return Obx(() { + // Wait for initialization to complete to prevent flickering + if (!widget.state.initialized.value) { + return const SizedBox.shrink(); + } + // If toolbar is hidden, return empty widget + if (hide.value) { + return const SizedBox.shrink(); + } + return Align( + alignment: Alignment.topCenter, + child: collapse.isFalse + ? _buildToolbar(context) + : _buildDraggableCollapse(context), + ); + }); } - Widget _buildDraggableShowHide(BuildContext context) { + Widget _buildDraggableCollapse(BuildContext context) { return Obx(() { - if (show.isTrue && _dragging.isFalse) { + if (collapse.isFalse && _dragging.isFalse) { triggerAutoHide(); } final borderRadius = BorderRadius.vertical( @@ -398,7 +435,7 @@ class _RemoteToolbarState extends State { ), ), ), - _buildDraggableShowHide(context), + _buildDraggableCollapse(context), ], ); } @@ -2491,7 +2528,7 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { double left = 0.0; double right = 1.0; - RxBool get show => widget.toolbarState.show; + RxBool get collapse => widget.toolbarState.collapse; @override initState() { @@ -2614,20 +2651,20 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { )), buttonWrapper( () => setState(() { - widget.toolbarState.switchShow(widget.sessionId); + widget.toolbarState.switchCollapse(widget.sessionId); }), Obx((() => Tooltip( - message: - translate(show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'), + message: translate( + collapse.isFalse ? 'Hide Toolbar' : 'Show Toolbar'), child: Icon( - show.isTrue ? Icons.expand_less : Icons.expand_more, + collapse.isFalse ? Icons.expand_less : Icons.expand_more, size: iconSize, ), ))), ), if (isWebDesktop) Obx(() { - if (show.isTrue) { + if (collapse.isFalse) { return Offstage(); } else { return buttonWrapper( From 4f2aea65ab634c46dde9ec628e8cef01da605e81 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 20 Dec 2025 16:51:25 +0800 Subject: [PATCH 035/277] require login for note (#13775) Signed-off-by: 21pages --- .../desktop/pages/desktop_setting_page.dart | 7 +++++ flutter/lib/mobile/pages/settings_page.dart | 4 +++ flutter/lib/models/model.dart | 8 +++++- src/lang/vi.rs | 26 ++++++++++++++++++- src/ui_session_interface.rs | 3 +++ 5 files changed, 46 insertions(+), 2 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 6e8f42d4e..82b7c75ee 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -566,6 +566,13 @@ class _GeneralState extends State<_General> { 'note-at-conn-end-tip', kOptionAllowAskForNoteAtEndOfConnection, isServer: false, + optSetter: (key, value) async { + if (value && !gFFI.userModel.isLogin) { + final res = await loginDialog(); + if (res != true) return; + } + await mainSetLocalBoolOption(key, value); + }, )); return _Card(title: 'Other', children: children); } diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 395b77962..9a237f44a 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -790,6 +790,10 @@ class _SettingsState extends State with WidgetsBindingObserver { title: Text(translate('note-at-conn-end-tip')), initialValue: _allowAskForNoteAtEndOfConnection, onToggle: (v) async { + if (v && !gFFI.userModel.isLogin) { + final res = await loginDialog(); + if (res != true) return; + } await mainSetLocalBoolOption( kOptionAllowAskForNoteAtEndOfConnection, v); final newValue = mainGetLocalBoolOptionSync( diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index b6d98a01c..5eba92cb7 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1081,7 +1081,8 @@ class FfiModel with ChangeNotifier { if (displays.length == 1) { bind.sessionSetSize( sessionId: sessionId, - display: pi.currentDisplay == kAllDisplayValue ? 0 : pi.currentDisplay, + display: + pi.currentDisplay == kAllDisplayValue ? 0 : pi.currentDisplay, width: displays[0].width, height: displays[0].height, ); @@ -1100,6 +1101,11 @@ class FfiModel with ChangeNotifier { void _queryAuditGuid(String peerId) async { try { + if (bind + .sessionGetAuditServerSync(sessionId: sessionId, typ: "conn/active") + .isEmpty) { + return; + } if (!mainGetLocalBoolOptionSync( kOptionAllowAskForNoteAtEndOfConnection)) { return; diff --git a/src/lang/vi.rs b/src/lang/vi.rs index 0f3ae4fec..58fb13656 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -539,7 +539,31 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Timeout in minutes", ""), ("auto_disconnect_option_tip", ""), ("Connection failed due to inactivity", ""), - ("Check for software update on startupmật", ""), + ("Check for software update on startup", ""), + ("upgrade_rustdesk_server_pro_to_{}_tip", ""), + ("pull_group_failed_tip", ""), + ("Filter by intersection", ""), + ("Remove wallpaper during incoming sessions", ""), + ("Test", ""), + ("display_is_plugged_out_msg", ""), + ("No displays", ""), + ("Open in new window", ""), + ("Show displays as individual windows", ""), + ("Use all my displays for the remote session", ""), + ("selinux_tip", ""), + ("Change view", ""), + ("Big tiles", ""), + ("Small tiles", ""), + ("List", ""), + ("Virtual display", ""), + ("Plug out all", ""), + ("True color (4:4:4)", ""), + ("Enable blocking user input", ""), + ("id_input_tip", ""), + ("privacy_mode_impl_mag_tip", ""), + ("privacy_mode_impl_virtual_display_tip", ""), + ("Enter privacy mode", ""), + ("Exit privacy mode", ""), ("idd_not_support_under_win10_2004_tip", ""), ("input_source_1_tip", ""), ("input_source_2_tip", ""), diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index be1baa587..88ee7bc9b 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -567,6 +567,9 @@ impl Session { } pub fn get_audit_server(&self, typ: String) -> String { + if LocalConfig::get_option("access_token").is_empty() { + return "".to_owned(); + } crate::get_audit_server( Config::get_option("api-server"), Config::get_option("custom-rendezvous-server"), From 84eb75d5b60ed7440d40b5d3ac7d61bb6cfcdfdf Mon Sep 17 00:00:00 2001 From: YuZhiYuanDev <203504060+YuZhiYuanDev@users.noreply.github.com> Date: Sat, 20 Dec 2025 21:21:14 +0800 Subject: [PATCH 036/277] ci: update macOS runner from unsupported macos-13 to macos-latest (#13855) - Replace deprecated `macos-13` with `macos-latest` runner - Ensure CI compatibility with supported macOS versions - Maintain build stability and future-proof workflows --- .github/workflows/flutter-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index fa2a622a0..df5b68eb4 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -444,7 +444,7 @@ jobs: - { arch: aarch64, target: aarch64-apple-ios, - os: macos-13, + os: macos-latest, vcpkg-triplet: arm64-ios, } steps: From 1f9689dc006f6d1d72a94bd6a433ff5178a3f4ee Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 21 Dec 2025 22:18:18 +0800 Subject: [PATCH 037/277] show login dialog when clicking note if not logged in (#13856) Signed-off-by: 21pages --- flutter/lib/common/widgets/toolbar.dart | 24 ++++++++++--- flutter/lib/consts.dart | 1 + .../lib/desktop/pages/desktop_home_page.dart | 2 ++ .../desktop/pages/desktop_setting_page.dart | 28 ++++++++------- flutter/lib/mobile/pages/settings_page.dart | 35 ++++++++++--------- flutter/lib/models/model.dart | 3 ++ 6 files changed, 58 insertions(+), 35 deletions(-) diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index b158679eb..929acbfcf 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -6,10 +6,12 @@ import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common/shared_state.dart'; import 'package:flutter_hbb/common/widgets/dialog.dart'; +import 'package:flutter_hbb/common/widgets/login.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; import 'package:flutter_hbb/models/model.dart'; import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:get/get.dart'; bool isEditOsPassword = false; @@ -193,14 +195,26 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { ); } // note - if (isDefaultConn && - bind - .sessionGetAuditServerSync(sessionId: sessionId, typ: "conn") - .isNotEmpty) { + if (isDefaultConn && !bind.isDisableAccount()) { v.add( TTextMenu( child: Text(translate('Note')), - onPressed: () => showAuditDialog(ffi)), + onPressed: () async { + bool isLogin = + bind.mainGetLocalOption(key: 'access_token').isNotEmpty; + if (!isLogin) { + final res = await loginDialog(); + if (res != true) return; + // Desktop: send message to main window to refresh login status + // Web: login is required before connection, so no need to refresh + // Mobile: same isolate, no need to send message + if (isDesktop) { + rustDeskWinManager.call( + WindowType.Main, kWindowRefreshCurrentUser, ""); + } + } + showAuditDialog(ffi); + }), ); } // divider diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 6c68d3d91..94a0aaac5 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -50,6 +50,7 @@ const String kAppTypeDesktopPortForward = "port forward"; const String kAppTypeDesktopTerminal = "terminal"; const String kWindowMainWindowOnTop = "main_window_on_top"; +const String kWindowRefreshCurrentUser = "refresh_current_user"; const String kWindowGetWindowInfo = "get_window_info"; const String kWindowGetScreenList = "get_screen_list"; // This method is not used, maybe it can be removed. diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index b8b7c0286..0a75175db 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -776,6 +776,8 @@ class _DesktopHomePageState extends State } if (call.method == kWindowMainWindowOnTop) { windowOnTop(null); + } else if (call.method == kWindowRefreshCurrentUser) { + gFFI.userModel.refreshCurrentUser(); } else if (call.method == kWindowGetWindowInfo) { final screen = (await window_size.getWindowInfo()).screen; if (screen == null) { diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 82b7c75ee..ab6dfe47e 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -561,19 +561,21 @@ class _GeneralState extends State<_General> { children.add(_OptionCheckBox( context, 'Allow linux headless', kOptionAllowLinuxHeadless)); } - children.add(_OptionCheckBox( - context, - 'note-at-conn-end-tip', - kOptionAllowAskForNoteAtEndOfConnection, - isServer: false, - optSetter: (key, value) async { - if (value && !gFFI.userModel.isLogin) { - final res = await loginDialog(); - if (res != true) return; - } - await mainSetLocalBoolOption(key, value); - }, - )); + if (!bind.isDisableAccount()) { + children.add(_OptionCheckBox( + context, + 'note-at-conn-end-tip', + kOptionAllowAskForNoteAtEndOfConnection, + isServer: false, + optSetter: (key, value) async { + if (value && !gFFI.userModel.isLogin) { + final res = await loginDialog(); + if (res != true) return; + } + await mainSetLocalBoolOption(key, value); + }, + )); + } return _Card(title: 'Other', children: children); } diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 9a237f44a..69a9d6a44 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -786,23 +786,24 @@ class _SettingsState extends State with WidgetsBindingObserver { showThemeSettings(gFFI.dialogManager); }, ), - SettingsTile.switchTile( - title: Text(translate('note-at-conn-end-tip')), - initialValue: _allowAskForNoteAtEndOfConnection, - onToggle: (v) async { - if (v && !gFFI.userModel.isLogin) { - final res = await loginDialog(); - if (res != true) return; - } - await mainSetLocalBoolOption( - kOptionAllowAskForNoteAtEndOfConnection, v); - final newValue = mainGetLocalBoolOptionSync( - kOptionAllowAskForNoteAtEndOfConnection); - setState(() { - _allowAskForNoteAtEndOfConnection = newValue; - }); - }, - ) + if (!bind.isDisableAccount()) + SettingsTile.switchTile( + title: Text(translate('note-at-conn-end-tip')), + initialValue: _allowAskForNoteAtEndOfConnection, + onToggle: (v) async { + if (v && !gFFI.userModel.isLogin) { + final res = await loginDialog(); + if (res != true) return; + } + await mainSetLocalBoolOption( + kOptionAllowAskForNoteAtEndOfConnection, v); + final newValue = mainGetLocalBoolOptionSync( + kOptionAllowAskForNoteAtEndOfConnection); + setState(() { + _allowAskForNoteAtEndOfConnection = newValue; + }); + }, + ) ]), if (isAndroid) SettingsSection(title: Text(translate('Hardware Codec')), tiles: [ diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 5eba92cb7..6e3d77c54 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1101,6 +1101,9 @@ class FfiModel with ChangeNotifier { void _queryAuditGuid(String peerId) async { try { + if (bind.isDisableAccount()) { + return; + } if (bind .sessionGetAuditServerSync(sessionId: sessionId, typ: "conn/active") .isEmpty) { From b80eb2dc6ccc5679e361f22622d52bcf49b652b7 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 22 Dec 2025 17:10:53 +0800 Subject: [PATCH 038/277] refact: remote toolbar icon (#13865) Signed-off-by: fufesou --- flutter/assets/keyboard.svg | 1 - flutter/assets/keyboard_mouse.svg | 1 + .../lib/desktop/widgets/remote_toolbar.dart | 20 ++++++++++++++----- 3 files changed, 16 insertions(+), 6 deletions(-) delete mode 100644 flutter/assets/keyboard.svg create mode 100644 flutter/assets/keyboard_mouse.svg diff --git a/flutter/assets/keyboard.svg b/flutter/assets/keyboard.svg deleted file mode 100644 index 0e94a5a62..000000000 --- a/flutter/assets/keyboard.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/flutter/assets/keyboard_mouse.svg b/flutter/assets/keyboard_mouse.svg new file mode 100644 index 000000000..f6a5b4b2b --- /dev/null +++ b/flutter/assets/keyboard_mouse.svg @@ -0,0 +1 @@ + diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 06675f9ec..8146e0d6f 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -1765,13 +1765,23 @@ class _KeyboardMenu extends StatelessWidget { Widget build(BuildContext context) { var ffiModel = Provider.of(context); if (!ffiModel.keyboard) return Offstage(); - toolbarToggles() => toolbarKeyboardToggles(ffi) - .map((e) => CkbMenuButton( - value: e.value, onChanged: e.onChanged, child: e.child, ffi: ffi)) - .toList(); + toolbarToggles() { + final toggles = toolbarKeyboardToggles(ffi) + .map((e) => CkbMenuButton( + value: e.value, + onChanged: e.onChanged, + child: e.child, + ffi: ffi) as Widget) + .toList(); + if (toggles.isNotEmpty) { + toggles.add(Divider()); + } + return toggles; + } + return _IconSubmenuButton( tooltip: 'Keyboard Settings', - svg: "assets/keyboard.svg", + svg: "assets/keyboard_mouse.svg", ffi: ffi, color: _ToolbarTheme.blueColor, hoverColor: _ToolbarTheme.hoverBlueColor, From eba847e62ec581bd0ea539d0d2a5a31828db8d2b Mon Sep 17 00:00:00 2001 From: alonginwind <100897495+alonginwind@users.noreply.github.com> Date: Mon, 22 Dec 2025 21:08:38 +0800 Subject: [PATCH 039/277] Fix Terminal top content overlapping with notch (SafeArea) (#13724) --- flutter/lib/mobile/pages/terminal_page.dart | 89 ++++++++++++++++----- 1 file changed, 67 insertions(+), 22 deletions(-) diff --git a/flutter/lib/mobile/pages/terminal_page.dart b/flutter/lib/mobile/pages/terminal_page.dart index 17d9bbedb..35dcb04bd 100644 --- a/flutter/lib/mobile/pages/terminal_page.dart +++ b/flutter/lib/mobile/pages/terminal_page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; +import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; @@ -29,9 +31,12 @@ class TerminalPage extends StatefulWidget { } class _TerminalPageState extends State - with AutomaticKeepAliveClientMixin { + with AutomaticKeepAliveClientMixin, WidgetsBindingObserver { late FFI _ffi; late TerminalModel _terminalModel; + double? _cellHeight; + double _sysKeyboardHeight = 0; + Timer? _keyboardDebounce; // For web only. // 'monospace' does not work on web, use Google Fonts, `??` is only for null safety. @@ -44,6 +49,7 @@ class _TerminalPageState extends State @override void initState() { super.initState(); + WidgetsBinding.instance.addObserver(this); debugPrint( '[TerminalPage] Initializing terminal ${widget.terminalId} for peer ${widget.id}'); @@ -62,6 +68,10 @@ class _TerminalPageState extends State debugPrint( '[TerminalPage] Terminal model created for terminal ${widget.terminalId}'); + _terminalModel.onResizeExternal = (w, h, pw, ph) { + _cellHeight = ph * 1.0; + }; + // Register this terminal model with FFI for event routing _ffi.registerTerminalModel(widget.terminalId, _terminalModel); @@ -78,10 +88,36 @@ class _TerminalPageState extends State // Unregister terminal model from FFI _ffi.unregisterTerminalModel(widget.terminalId); _terminalModel.dispose(); + _keyboardDebounce?.cancel(); + WidgetsBinding.instance.removeObserver(this); super.dispose(); TerminalConnectionManager.releaseConnection(widget.id); } + @override + void didChangeMetrics() { + super.didChangeMetrics(); + + _keyboardDebounce?.cancel(); + _keyboardDebounce = Timer(const Duration(milliseconds: 20), () { + final bottomInset = MediaQuery.of(context).viewInsets.bottom; + setState(() { + _sysKeyboardHeight = bottomInset; + }); + }); + } + + EdgeInsets _calculatePadding(double heightPx) { + if (_cellHeight == null) { + return const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0); + } + final realHeight = heightPx - _sysKeyboardHeight; + final rows = (realHeight / _cellHeight!).floor(); + final extraSpace = realHeight - rows * _cellHeight!; + final topBottom = max(0.0, extraSpace / 2.0); + return EdgeInsets.only(left: 5.0, right: 5.0, top: topBottom, bottom: topBottom + _sysKeyboardHeight); + } + @override Widget build(BuildContext context) { super.build(context); @@ -96,28 +132,37 @@ class _TerminalPageState extends State Widget buildBody() { return Scaffold( + resizeToAvoidBottomInset: false, // Disable automatic layout adjustment; manually control UI updates to prevent flickering when the keyboard shows/hides backgroundColor: Theme.of(context).scaffoldBackgroundColor, - body: TerminalView( - _terminalModel.terminal, - controller: _terminalModel.terminalController, - autofocus: true, - textStyle: _getTerminalStyle(), - backgroundOpacity: 0.7, - padding: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0), - onSecondaryTapDown: (details, offset) async { - final selection = _terminalModel.terminalController.selection; - if (selection != null) { - final text = _terminalModel.terminal.buffer.getText(selection); - _terminalModel.terminalController.clearSelection(); - await Clipboard.setData(ClipboardData(text: text)); - } else { - final data = await Clipboard.getData('text/plain'); - final text = data?.text; - if (text != null) { - _terminalModel.terminal.paste(text); - } - } - }, + body: SafeArea( + top: true, + child: LayoutBuilder( + builder: (context, constraints) { + final heightPx = constraints.maxHeight; + return TerminalView( + _terminalModel.terminal, + controller: _terminalModel.terminalController, + autofocus: true, + textStyle: _getTerminalStyle(), + backgroundOpacity: 0.7, + padding: _calculatePadding(heightPx), + onSecondaryTapDown: (details, offset) async { + final selection = _terminalModel.terminalController.selection; + if (selection != null) { + final text = _terminalModel.terminal.buffer.getText(selection); + _terminalModel.terminalController.clearSelection(); + await Clipboard.setData(ClipboardData(text: text)); + } else { + final data = await Clipboard.getData('text/plain'); + final text = data?.text; + if (text != null) { + _terminalModel.terminal.paste(text); + } + } + }, + ); + }, + ), ), ); } From 6a701f1420360fe4c6f2d8c812670c2177d6becc Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 23 Dec 2025 15:43:31 +0800 Subject: [PATCH 040/277] fix: linux, home (#13879) Signed-off-by: fufesou --- Cargo.lock | 2 +- Cargo.toml | 1 - libs/hbb_common | 2 +- src/platform/linux.rs | 69 ++++++++++++++++++++------- src/platform/linux_desktop_manager.rs | 8 +++- 5 files changed, 61 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e3f95bc26..e3e40ec06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3749,6 +3749,7 @@ dependencies = [ "toml 0.7.8", "tungstenite", "url", + "users 0.11.0", "uuid", "webpki-roots 1.0.4", "webrtc", @@ -7231,7 +7232,6 @@ dependencies = [ "tray-icon", "ttf-parser", "url", - "users 0.11.0", "uuid", "virtual_display", "wallpaper", diff --git a/Cargo.toml b/Cargo.toml index 801ab8cdf..0b63a8167 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -176,7 +176,6 @@ evdev = { git="https://github.com/rustdesk-org/evdev" } dbus = "0.9" dbus-crossroads = "0.5" pam = { git="https://github.com/rustdesk-org/pam" } -users = { version = "0.11" } x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true} x11rb = {version = "0.12", features = ["all-extensions"], optional = true} percent-encoding = {version = "2.3", optional = true} diff --git a/libs/hbb_common b/libs/hbb_common index 8b0e25867..fa157108b 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 8b0e25867375ba9e6bff548acf44fe6d6ffa7c0e +Subproject commit fa157108be16b9ce58852a69c2186a3ced3c559b diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 569c20f9f..d5a5edac0 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -1,21 +1,20 @@ use super::{gtk_sudo, CursorData, ResultType}; use desktop::Desktop; -use hbb_common::config::keys::OPTION_ALLOW_LINUX_HEADLESS; pub use hbb_common::platform::linux::*; use hbb_common::{ allow_err, anyhow::anyhow, bail, - config::Config, + config::{keys::OPTION_ALLOW_LINUX_HEADLESS, Config}, libc::{c_char, c_int, c_long, c_void}, log, message_proto::{DisplayInfo, Resolution}, regex::{Captures, Regex}, + users::{get_user_by_name, os::unix::UserExt}, }; use std::{ cell::RefCell, ffi::{OsStr, OsString}, - os::unix::ffi::OsStrExt, path::{Path, PathBuf}, process::{Child, Command}, string::String, @@ -26,7 +25,6 @@ use std::{ time::{Duration, Instant}, }; use terminfo::{capability as cap, Database}; -use users::{get_user_by_name, os::unix::UserExt}; use wallpaper; type Xdo = *const c_void; @@ -1714,26 +1712,57 @@ pub fn run_cmds_privileged(cmds: &str) -> bool { crate::platform::gtk_sudo::run(vec![cmds]).is_ok() } +/// Spawn the current executable after a delay. +/// +/// # Security +/// The executable path is safely quoted using `shell_quote()` to prevent +/// command injection vulnerabilities. The `secs` parameter is a u32, so it +/// cannot contain malicious input. +/// +/// # Arguments +/// * `secs` - Number of seconds to wait before spawning pub fn run_me_with(secs: u32) { - let exe = std::env::current_exe() - .unwrap_or("".into()) - .to_string_lossy() - .to_string(); - // We use `CMD_SH` instead of `sh` to suppress some audit messages on some systems. - std::process::Command::new(CMD_SH.as_str()) + let exe = match std::env::current_exe() { + Ok(path) => path, + Err(e) => { + log::error!("Failed to get current exe: {}", e); + return; + } + }; + + // SECURITY: Use shell_quote to safely escape the executable path, + // preventing command injection even if the path contains special characters. + let exe_quoted = shell_quote(&exe.to_string_lossy()); + + // Spawn a background process that sleeps and then executes. + // The child process is automatically orphaned when parent exits, + // and will be adopted by init (PID 1). + Command::new(CMD_SH.as_str()) .arg("-c") - .arg(&format!("sleep {secs}; {exe}")) + .arg(&format!("sleep {secs}; exec {exe_quoted}")) .spawn() .ok(); } fn switch_service(stop: bool) -> String { - let home = std::env::var("HOME").unwrap_or_default(); + // SECURITY: Use trusted home directory lookup via getpwuid instead of $HOME env var + // to prevent confused-deputy attacks where an attacker manipulates environment variables. + let home = get_home_dir_trusted() + .map(|p| p.to_string_lossy().to_string()) + .unwrap_or_default(); Config::set_option("stop-service".into(), if stop { "Y" } else { "" }.into()); - if home != "/root" && !Config::get().is_empty() { - let p = format!(".config/{}", crate::get_app_name().to_lowercase()); + if !home.is_empty() && home != "/root" && !Config::get().is_empty() { + let app_name_lower = crate::get_app_name().to_lowercase(); let app_name0 = crate::get_app_name(); - format!("cp -f {home}/{p}/{app_name0}.toml /root/{p}/; cp -f {home}/{p}/{app_name0}2.toml /root/{p}/;") + let config_subdir = format!(".config/{}", app_name_lower); + + // SECURITY: Quote all paths to prevent shell injection from paths containing + // spaces, semicolons, or other special characters. + let src1 = shell_quote(&format!("{}/{}/{}.toml", home, config_subdir, app_name0)); + let src2 = shell_quote(&format!("{}/{}/{}2.toml", home, config_subdir, app_name0)); + let dst = shell_quote(&format!("/root/{}/", config_subdir)); + + format!("cp -f {} {}; cp -f {} {};", src1, dst, src2, dst) } else { "".to_owned() } @@ -1787,7 +1816,15 @@ fn check_if_stop_service() { } pub fn check_autostart_config() -> ResultType<()> { - let home = std::env::var("HOME").unwrap_or_default(); + // SECURITY: Use trusted home directory lookup via getpwuid instead of $HOME env var + // to prevent confused-deputy attacks where an attacker manipulates environment variables. + let home = match get_home_dir_trusted() { + Some(p) => p.to_string_lossy().to_string(), + None => { + log::warn!("Failed to get trusted home directory for autostart config check"); + return Ok(()); + } + }; let app_name = crate::get_app_name().to_lowercase(); let path = format!("{home}/.config/autostart"); let file = format!("{path}/{app_name}.desktop"); diff --git a/src/platform/linux_desktop_manager.rs b/src/platform/linux_desktop_manager.rs index 6e21321da..03f1f6250 100644 --- a/src/platform/linux_desktop_manager.rs +++ b/src/platform/linux_desktop_manager.rs @@ -4,7 +4,12 @@ use crate::client::{ LOGIN_MSG_DESKTOP_SESSION_NOT_READY, LOGIN_MSG_DESKTOP_XORG_NOT_FOUND, LOGIN_MSG_DESKTOP_XSESSION_FAILED, }; -use hbb_common::{allow_err, bail, log, rand::prelude::*, tokio::time}; +use hbb_common::{ + allow_err, bail, log, + rand::prelude::*, + tokio::time, + users::{get_user_by_name, os::unix::UserExt, User}, +}; use pam; use std::{ collections::HashMap, @@ -18,7 +23,6 @@ use std::{ }, time::{Duration, Instant}, }; -use users::{get_user_by_name, os::unix::UserExt, User}; lazy_static::lazy_static! { static ref DESKTOP_RUNNING: Arc = Arc::new(AtomicBool::new(false)); From bba57069a892ea91234b24622a260946d353d735 Mon Sep 17 00:00:00 2001 From: lif <1835304752@qq.com> Date: Wed, 24 Dec 2025 18:18:51 +0800 Subject: [PATCH 041/277] fix: set TERM env variable for terminal to fix Delete key not working (#13747) Set TERM=xterm-256color when spawning PTY shell to ensure proper handling of control sequences. This fixes the issue where Delete/ Backspace keys were not working in terminal connections, particularly from iPad to Linux. Fixes #13621 --- src/server/terminal_service.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/server/terminal_service.rs b/src/server/terminal_service.rs index 945ae27bd..959d387f5 100644 --- a/src/server/terminal_service.rs +++ b/src/server/terminal_service.rs @@ -774,6 +774,11 @@ impl TerminalServiceProxy { #[allow(unused_mut)] let mut cmd = CommandBuilder::new(&shell); + // Set TERM environment variable to ensure proper handling of control sequences + // This fixes issues with Delete/Backspace keys not working correctly + // See: https://github.com/rustdesk/rustdesk/issues/13621 + cmd.env("TERM", "xterm-256color"); + #[cfg(target_os = "windows")] if let Some(token) = &self.user_token { cmd.set_user_token(*token as _); From b69e871f9a7c60ebdb2907bfeb37dd05d47ca9fa Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Wed, 24 Dec 2025 22:59:13 +0800 Subject: [PATCH 042/277] =?UTF-8?q?Revert=20"fix:=20set=20TERM=20env=20var?= =?UTF-8?q?iable=20for=20terminal=20to=20fix=20Delete=20key=20not=20workin?= =?UTF-8?q?g=E2=80=A6"=20(#13894)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit bba57069a892ea91234b24622a260946d353d735. --- src/server/terminal_service.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/server/terminal_service.rs b/src/server/terminal_service.rs index 959d387f5..945ae27bd 100644 --- a/src/server/terminal_service.rs +++ b/src/server/terminal_service.rs @@ -774,11 +774,6 @@ impl TerminalServiceProxy { #[allow(unused_mut)] let mut cmd = CommandBuilder::new(&shell); - // Set TERM environment variable to ensure proper handling of control sequences - // This fixes issues with Delete/Backspace keys not working correctly - // See: https://github.com/rustdesk/rustdesk/issues/13621 - cmd.env("TERM", "xterm-256color"); - #[cfg(target_os = "windows")] if let Some(token) = &self.user_token { cmd.set_user_token(*token as _); From 656ce93d6e335bcbe3728612336dd505fe13a4b6 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 25 Dec 2025 17:10:49 +0800 Subject: [PATCH 043/277] refact: ci, free disk space(Ubuntu) (#13900) Signed-off-by: fufesou --- .github/workflows/ci.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 157bac491..3a7d21d7e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,6 +84,20 @@ jobs: - { target: x86_64-unknown-linux-gnu , os: ubuntu-24.04 } # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } steps: + - name: Free Disk Space (Ubuntu) + if: runner.os == 'Linux' + # jlumbroso/free-disk-space@main is used in .github\workflows\flutter-build.yml + # But pinning to a specific version to avoid unexpected issues is preferred. + uses: jlumbroso/free-disk-space@v1.3.1 + with: + tool-cache: false + android: true + dotnet: true + haskell: true + large-packages: false + docker-images: true + swap-storage: false + - name: Export GitHub Actions cache environment variables uses: actions/github-script@v6 with: From ec2d7f0519b3daeffb7183636950290b6aa67fd5 Mon Sep 17 00:00:00 2001 From: Andrzej Rudnik Date: Fri, 26 Dec 2025 06:31:49 +0100 Subject: [PATCH 044/277] Update pl.rs (#13893) --- src/lang/pl.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 3732184a1..b209dc7d6 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -721,13 +721,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", "Pokaz wirtualny joystick"), ("Edit note", "Edytuj notatkę"), ("Alias", "Alias"), - ("ScrollEdge", ""), - ("Allow insecure TLS fallback", ""), - ("allow-insecure-tls-fallback-tip", ""), - ("Disable UDP", ""), - ("disable-udp-tip", ""), - ("server-oss-not-support-tip", ""), - ("input note here", ""), - ("note-at-conn-end-tip", ""), + ("ScrollEdge", "Przewijanie na krawędzi"), + ("Allow insecure TLS fallback", "Zezwól na nie zweryfikowane połączenia TLS"), + ("allow-insecure-tls-fallback-tip", "Domyślnie RustDesk weryfikuje certyfikat serwera dla protokołów korzystających z TLS.\n Po włączeniu tej opcji, RustDesk pominie etap weryfikacji i będzie kontynuował działanie w przypadku negatywnej weryfikacji."), + ("Disable UDP", "Wyłącz protokół UDP"), + ("disable-udp-tip", "Kontroluje, czy używać wyłącznie protokołu TCP.\nPo włączeniu tej opcji, RustDesk nie będzie używać protokołu UDP 21116, zamiast niego będzie używać protokołu TCP 21116."), + ("server-oss-not-support-tip", "UWAGA: Serwer OSS RustDesk nie obsługuje tej funkcji."), + ("input note here", "Wstaw tutaj notatkę"), + ("note-at-conn-end-tip", "Poproś o notatkę po zakończeniu połączenia."), ].iter().cloned().collect(); } From 5b2101e17d14552966978a99eb39f1d1e8e6c60f Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 26 Dec 2025 15:28:35 +0800 Subject: [PATCH 045/277] fix(terminal): macos, env TERM (#13901) Signed-off-by: fufesou --- src/server/terminal_service.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/server/terminal_service.rs b/src/server/terminal_service.rs index 945ae27bd..194e41ef1 100644 --- a/src/server/terminal_service.rs +++ b/src/server/terminal_service.rs @@ -774,6 +774,21 @@ impl TerminalServiceProxy { #[allow(unused_mut)] let mut cmd = CommandBuilder::new(&shell); + // Set `TERM` environment variable for macOS to ensure proper terminal behavior + // This fixes issues with control sequences (e.g., Delete/Backspace keys) + // macOS terminfo uses hex naming: '78' = 'x' for xterm entries + // Note: For Linux, `TERM` is set in src/platform/linux.rs try_start_server_() + #[cfg(target_os = "macos")] + { + let term = if std::path::Path::new("/usr/share/terminfo/78/xterm-256color").exists() { + "xterm-256color" + } else { + "xterm" + }; + cmd.env("TERM", term); + log::debug!("Set TERM={} for macOS PTY", term); + } + #[cfg(target_os = "windows")] if let Some(token) = &self.user_token { cmd.set_user_token(*token as _); From 969ea28d064688c97c1784596f330ac69bc8b76d Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sun, 28 Dec 2025 15:39:35 +0800 Subject: [PATCH 046/277] feat(fs): delegate win --server file reading to CM (#13736) - Route Windows server-to-client file reads through CM instead of the connection layer - Add FS IPC commands (ReadFile, CancelRead, SendConfirmForRead, ReadAllFiles) and CM data messages (ReadJobInitResult, FileBlockFromCM, FileReadDone, FileReadError, FileDigestFromCM, AllFilesResult) - Track pending read validations and read jobs to coordinate CM-driven file transfers and clean them up on completion, cancellation, and errors - Enforce a configurable file-transfer-max-files limit for ReadAllFiles and add stronger file name/path validation on the CM side - Improve Flutter file transfer UX and robustness: - Use explicit percent/percentText progress fields - Derive speed and cancel actions from the active job - Handle job errors via FileModel.handleJobError and complete pending recursive tasks on failure - Wrap recursive directory operations in try/catch and await sendRemoveEmptyDir when removing empty directories Signed-off-by: fufesou --- .../lib/desktop/pages/file_manager_page.dart | 6 +- .../lib/mobile/pages/file_manager_page.dart | 14 +- flutter/lib/models/cm_file_model.dart | 2 +- flutter/lib/models/file_model.dart | 68 +- flutter/lib/models/model.dart | 2 +- src/client/io_loop.rs | 1 + src/common.rs | 12 + src/ipc.rs | 93 +++ src/server/connection.rs | 457 ++++++++-- src/server/input_service.rs | 7 +- src/ui_cm_interface.rs | 784 +++++++++++++++++- 11 files changed, 1349 insertions(+), 97 deletions(-) diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 6dc89d09f..9e554cbe8 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -282,11 +282,9 @@ class _FileManagerPageState extends State item.state != JobState.inProgress, child: LinearPercentIndicator( animateFromLastPercent: true, - center: Text( - '${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%', - ), + center: Text(item.percentText), barRadius: Radius.circular(15), - percent: item.finishedSize / item.totalSize, + percent: item.percent, progressColor: MyTheme.accent, backgroundColor: Theme.of(context).hoverColor, lineHeight: kDesktopFileTransferRowHeight, diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index c7b183d35..745df67b5 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -355,15 +355,21 @@ class _FileManagerPageState extends State { return Offstage(); } - switch (jobTable.last.state) { + // Find the first job that is in progress (the one actually transferring data) + // Rust backend processes jobs sequentially, so the first inProgress job is the active one + final activeJob = jobTable + .firstWhereOrNull((job) => job.state == JobState.inProgress) ?? + jobTable.last; + + switch (activeJob.state) { case JobState.inProgress: return BottomSheetBody( leading: CircularProgressIndicator(), title: translate("Waiting"), text: - "${translate("Speed")}: ${readableFileSize(jobTable.last.speed)}/s", + "${translate("Speed")}: ${readableFileSize(activeJob.speed)}/s", onCanceled: () { - model.jobController.cancelJob(jobTable.last.id); + model.jobController.cancelJob(activeJob.id); jobTable.clear(); }, ); @@ -371,7 +377,7 @@ class _FileManagerPageState extends State { return BottomSheetBody( leading: Icon(Icons.check), title: "${translate("Successful")}!", - text: jobTable.last.display(), + text: activeJob.display(), onCanceled: () => jobTable.clear(), ); case JobState.error: diff --git a/flutter/lib/models/cm_file_model.dart b/flutter/lib/models/cm_file_model.dart index 6609f1191..46935c188 100644 --- a/flutter/lib/models/cm_file_model.dart +++ b/flutter/lib/models/cm_file_model.dart @@ -275,7 +275,7 @@ class TransferJobSerdeData { : this( connId: d['connId'] ?? 0, id: int.tryParse(d['id'].toString()) ?? 0, - path: d['path'] ?? '', + path: d['dataSource'] ?? '', isRemote: d['isRemote'] ?? false, totalSize: d['totalSize'] ?? 0, finishedSize: d['finishedSize'] ?? 0, diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index d2ae7cff2..35001cbf2 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -113,6 +113,34 @@ class FileModel { fileFetcher.tryCompleteEmptyDirsTask(evt['value'], evt['is_local']); } + // This method fixes a deadlock that occurred when the previous code directly + // called jobController.jobError(evt) in the job_error event handler. + // + // The problem with directly calling jobController.jobError(): + // 1. fetchDirectoryRecursiveToRemove(jobID) registers readRecursiveTasks[jobID] + // and waits for completion + // 2. If the remote has no permission (or some other errors), it returns a FileTransferError + // 3. The error triggers job_error event, which called jobController.jobError() + // 4. jobController.jobError() calls getJob(jobID) to find the job in jobTable + // 5. But addDeleteDirJob() is called AFTER fetchDirectoryRecursiveToRemove(), + // so the job doesn't exist yet in jobTable + // 6. Result: jobController.jobError() does nothing useful, and + // readRecursiveTasks[jobID] never completes, causing a 2s timeout + // + // Solution: Before calling jobController.jobError(), we first check if there's + // a pending readRecursiveTasks with this ID and complete it with the error. + void handleJobError(Map evt) { + final id = int.tryParse(evt['id']?.toString() ?? ''); + if (id != null) { + final err = evt['err']?.toString() ?? 'Unknown error'; + fileFetcher.tryCompleteRecursiveTaskWithError(id, err); + } + // Always call jobController.jobError(evt) to ensure all error events are processed, + // even if the event does not have a valid job ID. This allows for generic error handling + // or logging of unexpected errors. + jobController.jobError(evt); + } + Future postOverrideFileConfirm(Map evt) async { evtLoop.pushEvent( _FileDialogEvent(WeakReference(this), FileDialogType.overwrite, evt)); @@ -591,8 +619,21 @@ class FileController { } else if (item.isDirectory) { title = translate("Not an empty directory"); dialogManager?.showLoading(translate("Waiting")); - final fd = await fileFetcher.fetchDirectoryRecursiveToRemove( - jobID, item.path, items.isLocal, true); + final FileDirectory fd; + try { + fd = await fileFetcher.fetchDirectoryRecursiveToRemove( + jobID, item.path, items.isLocal, true); + } catch (e) { + dialogManager?.dismissAll(); + final dm = dialogManager; + if (dm != null) { + msgBox(sessionId, 'custom-error-nook-nocancel-hasclose', + translate("Error"), e.toString(), '', dm); + } else { + debugPrint("removeAction error msgbox failed: $e"); + } + return; + } if (fd.path.isEmpty) { fd.path = item.path; } @@ -606,7 +647,7 @@ class FileController { item.name, false); if (confirm == true) { - sendRemoveEmptyDir( + await sendRemoveEmptyDir( item.path, 0, deleteJobId, @@ -647,7 +688,7 @@ class FileController { // handle remove res; if (item.isDirectory && res['file_num'] == (entries.length - 1).toString()) { - sendRemoveEmptyDir(item.path, i, deleteJobId); + await sendRemoveEmptyDir(item.path, i, deleteJobId); } } else { jobController.updateJobStatus(deleteJobId, @@ -660,7 +701,7 @@ class FileController { final res = await jobController.jobResultListener.start(); if (item.isDirectory && res['file_num'] == (entries.length - 1).toString()) { - sendRemoveEmptyDir(item.path, i, deleteJobId); + await sendRemoveEmptyDir(item.path, i, deleteJobId); } } } else { @@ -755,9 +796,9 @@ class FileController { fileNum: fileNum); } - void sendRemoveEmptyDir(String path, int fileNum, int actId) { + Future sendRemoveEmptyDir(String path, int fileNum, int actId) async { history.removeWhere((element) => element.contains(path)); - bind.sessionRemoveAllEmptyDirs( + await bind.sessionRemoveAllEmptyDirs( sessionId: sessionId, actId: actId, path: path, isRemote: !isLocal); } @@ -1275,6 +1316,15 @@ class FileFetcher { } } + // Complete a pending recursive read task with an error. + // See FileModel.handleJobError() for why this is necessary. + void tryCompleteRecursiveTaskWithError(int id, String error) { + final completer = readRecursiveTasks.remove(id); + if (completer != null && !completer.isCompleted) { + completer.completeError(error); + } + } + Future> readEmptyDirs( String path, bool isLocal, bool showHidden) async { try { @@ -1438,6 +1488,10 @@ class JobProgress { var err = ""; int lastTransferredSize = 0; + double get percent => + totalSize > 0 ? (finishedSize.toDouble() / totalSize) : 0.0; + String get percentText => '${(percent * 100).toStringAsFixed(0)}%'; + clear() { type = JobType.none; state = JobState.none; diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 6e3d77c54..e2f509c13 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -363,7 +363,7 @@ class FfiModel with ChangeNotifier { parent.target?.fileModel.refreshAll(); } } else if (name == 'job_error') { - parent.target?.fileModel.jobController.jobError(evt); + parent.target?.fileModel.handleJobError(evt); } else if (name == 'override_file_confirm') { parent.target?.fileModel.postOverrideFileConfirm(evt); } else if (name == 'load_last_job') { diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 2b52c7233..e0b3fcd6d 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1676,6 +1676,7 @@ impl Remote { } Some(file_response::Union::Error(e)) => { let job_type = fs::remove_job(e.id, &mut self.write_jobs) + .or_else(|| fs::remove_job(e.id, &mut self.read_jobs)) .map(|j| j.r#type) .unwrap_or(fs::JobType::Generic); match job_type { diff --git a/src/common.rs b/src/common.rs index 6decd2d04..0dc944d83 100644 --- a/src/common.rs +++ b/src/common.rs @@ -181,6 +181,18 @@ pub fn is_server() -> bool { *IS_SERVER } +#[inline] +pub fn need_fs_cm_send_files() -> bool { + #[cfg(windows)] + { + is_server() + } + #[cfg(not(windows))] + { + false + } +} + #[inline] pub fn is_main() -> bool { *IS_MAIN diff --git a/src/ipc.rs b/src/ipc.rs index 2281686ac..e5f163c2e 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -112,6 +112,33 @@ pub enum FS { path: String, new_name: String, }, + // CM-side file reading operations (Windows only) + // These enable Connection Manager to read files and stream them back to Connection + ReadFile { + path: String, + id: i32, + file_num: i32, + include_hidden: bool, + conn_id: i32, + overwrite_detection: bool, + }, + CancelRead { + id: i32, + conn_id: i32, + }, + SendConfirmForRead { + id: i32, + file_num: i32, + skip: bool, + offset_blk: u32, + conn_id: i32, + }, + ReadAllFiles { + path: String, + id: i32, + include_hidden: bool, + conn_id: i32, + }, } #[cfg(target_os = "windows")] @@ -268,6 +295,72 @@ pub enum Data { #[cfg(windows)] ControlledSessionCount(usize), CmErr(String), + // CM-side file reading responses (Windows only) + // These are sent from CM back to Connection when CM handles file reading + /// Response to ReadFile: contains initial file list or error + ReadJobInitResult { + id: i32, + file_num: i32, + include_hidden: bool, + conn_id: i32, + /// Serialized protobuf bytes of FileDirectory, or error string + result: Result, String>, + }, + /// File data block read by CM. + /// + /// The actual data is sent separately via `send_raw()` after this message to avoid + /// JSON encoding overhead for large binary data. This mirrors the `WriteBlock` pattern. + /// + /// **Protocol:** + /// - Sender: `send(FileBlockFromCM{...})` then `send_raw(data)` + /// - Receiver: `next()` returns `FileBlockFromCM`, then `next_raw()` returns data bytes + /// + /// **Note on empty data (e.g., empty files):** + /// Empty data is supported. The IPC connection uses `BytesCodec` with `raw=false` (default), + /// which prefixes each frame with a length header. So `send_raw(Bytes::new())` sends a + /// 1-byte frame (length=0), and `next_raw()` correctly returns an empty `BytesMut`. + /// See `libs/hbb_common/src/bytes_codec.rs` test `test_codec2` for verification. + FileBlockFromCM { + id: i32, + file_num: i32, + /// Data is sent separately via `send_raw()` to avoid JSON encoding overhead. + /// This field is skipped during serialization; sender must call `send_raw()` after sending. + /// Receiver must call `next_raw()` and populate this field manually. + #[serde(skip)] + data: bytes::Bytes, + compressed: bool, + conn_id: i32, + }, + /// File read completed successfully + FileReadDone { + id: i32, + file_num: i32, + conn_id: i32, + }, + /// File read failed with error + FileReadError { + id: i32, + file_num: i32, + err: String, + conn_id: i32, + }, + /// Digest info from CM for overwrite detection + FileDigestFromCM { + id: i32, + file_num: i32, + last_modified: u64, + file_size: u64, + is_resume: bool, + conn_id: i32, + }, + /// Response to ReadAllFiles: recursive directory listing + AllFilesResult { + id: i32, + conn_id: i32, + path: String, + /// Serialized protobuf bytes of FileDirectory, or error string + result: Result, String>, + }, CheckHwcodec, #[cfg(feature = "flutter")] VideoConnCount(Option), diff --git a/src/server/connection.rs b/src/server/connection.rs index af4892eb0..3670fb7cf 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -50,6 +50,7 @@ use serde_json::{json, value::Value}; #[cfg(not(any(target_os = "android", target_os = "ios")))] use std::sync::atomic::Ordering; use std::{ + collections::HashSet, net::Ipv6Addr, num::NonZeroI64, path::PathBuf, @@ -63,8 +64,6 @@ use windows::Win32::Foundation::{CloseHandle, HANDLE}; #[cfg(windows)] use crate::virtual_display_manager; -#[cfg(not(any(target_os = "ios")))] -use std::collections::HashSet; pub type Sender = mpsc::UnboundedSender<(Instant, Arc)>; lazy_static::lazy_static! { @@ -287,6 +286,11 @@ pub struct Connection { // For post requests that need to be sent sequentially. // eg. post_conn_audit tx_post_seq: mpsc::UnboundedSender<(String, Value)>, + // Tracks read job IDs delegated to CM process. + // When a read job is delegated to CM (via FS::ReadFile), the job id is added here. + // Used to filter stale responses (FileBlockFromCM, FileReadDone, etc.) for + // cancelled or unknown jobs. + cm_read_job_ids: HashSet, terminal_service_id: String, terminal_persistent: bool, // The user token must be set when terminal is enabled. @@ -459,6 +463,7 @@ impl Connection { tx_from_authed, printer_data: Vec::new(), tx_post_seq, + cm_read_job_ids: HashSet::new(), terminal_service_id: "".to_owned(), terminal_persistent: false, #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -717,6 +722,36 @@ impl Connection { let msg = new_voice_call_request(false); conn.send(msg).await; } + ipc::Data::ReadJobInitResult { id, file_num, include_hidden, conn_id, result } => { + if conn_id == conn.inner.id() { + conn.handle_read_job_init_result(id, file_num, include_hidden, result).await; + } + } + ipc::Data::FileBlockFromCM { id, file_num, data, compressed, conn_id } => { + if conn_id == conn.inner.id() { + conn.handle_file_block_from_cm(id, file_num, data, compressed).await; + } + } + ipc::Data::FileReadDone { id, file_num, conn_id } => { + if conn_id == conn.inner.id() { + conn.handle_file_read_done(id, file_num).await; + } + } + ipc::Data::FileReadError { id, file_num, err, conn_id } => { + if conn_id == conn.inner.id() { + conn.handle_file_read_error(id, file_num, err).await; + } + } + ipc::Data::FileDigestFromCM { id, file_num, last_modified, file_size, is_resume, conn_id } => { + if conn_id == conn.inner.id() { + conn.handle_file_digest_from_cm(id, file_num, last_modified, file_size, is_resume).await; + } + } + ipc::Data::AllFilesResult { id, conn_id, path, result } => { + if conn_id == conn.inner.id() { + conn.handle_all_files_result(id, path, result).await; + } + } _ => {} } }, @@ -2666,28 +2701,74 @@ impl Connection { self.read_dir(&rd.path, rd.include_hidden); } Some(file_action::Union::AllFiles(f)) => { - match fs::get_recursive_files(&f.path, f.include_hidden) { - Err(err) => { - self.send(fs::new_error(f.id, err, -1)).await; - } - Ok(files) => { - self.send(fs::new_dir(f.id, f.path, files)).await; + if crate::common::need_fs_cm_send_files() { + self.send_fs(ipc::FS::ReadAllFiles { + path: f.path, + id: f.id, + include_hidden: f.include_hidden, + conn_id: self.inner.id(), + }); + } else { + match fs::get_recursive_files(&f.path, f.include_hidden) { + Err(err) => { + log::error!( + "Failed to get recursive files for {}: {}", + f.path, + err + ); + self.send(fs::new_error(f.id, err, -1)).await; + } + Ok(files) => { + if let Err(msg) = + crate::ui_cm_interface::check_file_count_limit( + files.len(), + ) + { + self.send(fs::new_error(f.id, msg, -1)).await; + } else { + self.send(fs::new_dir(f.id, f.path, files)).await; + } + } } } } Some(file_action::Union::Send(s)) => { // server to client let id = s.id; - let od = can_enable_overwrite_detection(get_version_number( - &self.lr.version, - )); let path = s.path.clone(); - let r#type = JobType::from_proto(s.file_type); - let data_source; - match r#type { + let job_type = JobType::from_proto(s.file_type); + match job_type { JobType::Generic => { - data_source = - fs::DataSource::FilePath(PathBuf::from(&path)); + let od = can_enable_overwrite_detection( + get_version_number(&self.lr.version), + ); + if crate::common::need_fs_cm_send_files() { + // Delegate file reading to CM on Windows + self.cm_read_job_ids.insert(id); + self.send_fs(ipc::FS::ReadFile { + path, + id, + file_num: s.file_num, + include_hidden: s.include_hidden, + conn_id: self.inner.id(), + overwrite_detection: od, + }); + } else { + // Handle file reading in Connection on non-Windows + let data_source = + fs::DataSource::FilePath(PathBuf::from(&path)); + self.create_and_start_read_job( + id, + job_type, + data_source, + s.file_num, + s.include_hidden, + od, + path, + true, // check file count limit + ) + .await; + } } JobType::Printer => { if let Some((_, _, data)) = self @@ -2696,49 +2777,26 @@ impl Connection { .position(|(_, p, _)| *p == path) .map(|index| self.printer_data.remove(index)) { - data_source = fs::DataSource::MemoryCursor( + let data_source = fs::DataSource::MemoryCursor( std::io::Cursor::new(data), ); + // Printer jobs don't need file count limit check + self.create_and_start_read_job( + id, + job_type, + data_source, + s.file_num, + s.include_hidden, + true, // always enable overwrite detection for printer + path, + false, // no file count limit for printer + ) + .await; } else { // Ignore this message if the printer data is not found return true; } } - }; - match fs::TransferJob::new_read( - id, - r#type, - "".to_string(), - data_source, - s.file_num, - s.include_hidden, - false, - od, - ) { - Err(err) => { - self.send(fs::new_error(id, err, 0)).await; - } - Ok(mut job) => { - self.send(fs::new_dir(id, path, job.files().to_vec())) - .await; - let files = job.files().to_owned(); - job.is_remote = true; - job.conn_id = self.inner.id(); - let job_type = job.r#type; - self.read_jobs.push(job); - self.file_timer = - crate::rustdesk_interval(time::interval(MILLI1)); - self.post_file_audit( - FileAuditType::RemoteSend, - if job_type == fs::JobType::Printer { - "Remote print" - } else { - &s.path - }, - Self::get_files_for_audit(job_type, files), - json!({}), - ); - } } self.file_transferred = true; } @@ -2805,6 +2863,11 @@ impl Connection { } Some(file_action::Union::Cancel(c)) => { self.send_fs(ipc::FS::CancelWrite { id: c.id }); + let _ = self.cm_read_job_ids.remove(&c.id); + self.send_fs(ipc::FS::CancelRead { + id: c.id, + conn_id: self.inner.id(), + }); if let Some(job) = fs::remove_job(c.id, &mut self.read_jobs) { self.send_to_cm(ipc::Data::FileTransferLog(( "transfer".to_string(), @@ -2815,6 +2878,15 @@ impl Connection { Some(file_action::Union::SendConfirm(r)) => { if let Some(job) = fs::get_job(r.id, &mut self.read_jobs) { job.confirm(&r).await; + } else if self.cm_read_job_ids.contains(&r.id) { + // Forward to CM for CM-read jobs + self.send_fs(ipc::FS::SendConfirmForRead { + id: r.id, + file_num: r.file_num, + skip: r.skip(), + offset_blk: r.offset_blk(), + conn_id: self.inner.id(), + }); } else { if let Ok(sc) = r.write_to_bytes() { self.send_fs(ipc::FS::SendConfirm(sc)); @@ -4013,6 +4085,219 @@ impl Connection { raii::AuthedConnID::check_remove_session(self.inner.id(), self.session_key()); } + async fn handle_read_job_init_result( + &mut self, + id: i32, + _file_num: i32, + _include_hidden: bool, + result: Result, String>, + ) { + // Check if this response is still expected (not stale/cancelled) + if !self.cm_read_job_ids.contains(&id) { + log::warn!( + "Received ReadJobInitResult for unknown or stale job id={}, ignoring", + id + ); + return; + } + + match result { + Err(error) => { + self.cm_read_job_ids.remove(&id); + self.send(fs::new_error(id, error, 0)).await; + } + Ok(dir_bytes) => { + // Deserialize FileDirectory from protobuf bytes + let dir = match FileDirectory::parse_from_bytes(&dir_bytes) { + Ok(d) => d, + Err(e) => { + log::error!("Failed to parse FileDirectory: {}", e); + self.cm_read_job_ids.remove(&id); + self.send(fs::new_error(id, "internal error".to_string(), 0)) + .await; + return; + } + }; + + let path_str = dir.path.clone(); + let file_entries: Vec = dir.entries.into(); + + // Send file directory to client + self.send(fs::new_dir(id, path_str.clone(), file_entries.clone())) + .await; + + // Post audit for file transfer + self.post_file_audit( + FileAuditType::RemoteSend, + &path_str, + Self::get_files_for_audit(fs::JobType::Generic, file_entries), + json!({}), + ); + + // CM will handle the actual file reading and send blocks via IPC + self.file_transferred = true; + } + } + } + + async fn handle_file_block_from_cm( + &mut self, + id: i32, + file_num: i32, + data: bytes::Bytes, + compressed: bool, + ) { + // Check if the job is still valid (not cancelled) + if !self.cm_read_job_ids.contains(&id) { + log::debug!( + "Dropping file block for cancelled/unknown job id={}, file_num={}", + id, + file_num + ); + return; + } + + // Forward file block to client + let mut block = FileTransferBlock::new(); + block.id = id; + block.file_num = file_num; + block.data = data.to_vec().into(); + block.compressed = compressed; + + let mut msg = Message::new(); + let mut fr = FileResponse::new(); + fr.set_block(block); + msg.set_file_response(fr); + self.send(msg).await; + } + + async fn handle_file_read_done(&mut self, id: i32, file_num: i32) { + // Drop stale completions for cancelled/unknown jobs + if !self.cm_read_job_ids.remove(&id) { + log::debug!( + "Dropping FileReadDone for cancelled/unknown job id={}, file_num={}", + id, + file_num + ); + return; + } + + // Forward done message to client + let mut done = FileTransferDone::new(); + done.id = id; + done.file_num = file_num; + + let mut msg = Message::new(); + let mut fr = FileResponse::new(); + fr.set_done(done); + msg.set_file_response(fr); + self.send(msg).await; + } + + async fn handle_file_read_error(&mut self, id: i32, file_num: i32, err: String) { + // Drop stale errors for cancelled/unknown jobs + if !self.cm_read_job_ids.remove(&id) { + log::debug!( + "Dropping FileReadError for cancelled/unknown job id={}, file_num={}", + id, + file_num + ); + return; + } + + // Forward error to client + self.send(fs::new_error(id, err, file_num)).await; + } + + async fn handle_file_digest_from_cm( + &mut self, + id: i32, + file_num: i32, + last_modified: u64, + file_size: u64, + is_resume: bool, + ) { + // Check if the job is still valid (not cancelled) + if !self.cm_read_job_ids.contains(&id) { + log::debug!( + "Dropping digest for cancelled/unknown job id={}, file_num={}", + id, + file_num + ); + return; + } + + // Forward digest to client for overwrite detection + let mut digest = FileTransferDigest::new(); + digest.id = id; + digest.file_num = file_num; + digest.last_modified = last_modified; + digest.file_size = file_size; + digest.is_upload = false; // Server sending to client + digest.is_resume = is_resume; + + let mut msg = Message::new(); + let mut fr = FileResponse::new(); + fr.set_digest(digest); + msg.set_file_response(fr); + self.send(msg).await; + } + + async fn process_new_read_job(&mut self, mut job: fs::TransferJob, path: String) { + let files = job.files().to_owned(); + let job_type = job.r#type; + self.send(fs::new_dir(job.id, path.clone(), files.clone())) + .await; + job.is_remote = true; + job.conn_id = self.inner.id(); + self.read_jobs.push(job); + self.file_timer = crate::rustdesk_interval(time::interval(MILLI1)); + let audit_path = if job_type == fs::JobType::Printer { + "Remote print".to_owned() + } else { + path + }; + self.post_file_audit( + FileAuditType::RemoteSend, + &audit_path, + Self::get_files_for_audit(job_type, files), + json!({}), + ); + } + + async fn handle_all_files_result( + &mut self, + id: i32, + path: String, + result: Result, String>, + ) { + match result { + Err(err) => { + self.send(fs::new_error(id, err, -1)).await; + } + Ok(bytes) => { + // Deserialize FileDirectory from protobuf bytes and send as FileResponse + match FileDirectory::parse_from_bytes(&bytes) { + Ok(fd) => { + let mut msg = Message::new(); + let mut fr = FileResponse::new(); + fr.set_dir(fd); + msg.set_file_response(fr); + self.send(msg).await; + } + Err(e) => { + self.send(fs::new_error( + id, + format!("deserialize failed for {}: {}", path, e), + -1, + )) + .await; + } + } + } + } + } + fn read_empty_dirs(&mut self, dir: &str, include_hidden: bool) { let dir = dir.to_string(); self.send_fs(ipc::FS::ReadEmptyDirs { @@ -4029,6 +4314,57 @@ impl Connection { }); } + /// Create a new read job and start processing it (Connection-side). + /// + /// This is a generic Connection-side read job creation helper used for: + /// - Generic file transfers on non-Windows platforms + /// - Printer jobs on all platforms (including Windows) + /// + /// On Windows, generic file reads are delegated to CM via `start_read_job()` in + /// `src/ui_cm_interface.rs` for elevated access. Printer jobs bypass this delegation + /// since they read from in-memory data (`MemoryCursor`), not the filesystem. + /// + /// Both Connection-side and CM-side implementations use `TransferJob::new_read()` + /// with similar parameters. When modifying job creation logic, ensure both paths + /// stay in sync. + async fn create_and_start_read_job( + &mut self, + id: i32, + job_type: fs::JobType, + data_source: fs::DataSource, + file_num: i32, + include_hidden: bool, + overwrite_detection: bool, + path: String, + check_file_limit: bool, + ) { + match fs::TransferJob::new_read( + id, + job_type, + "".to_string(), + data_source, + file_num, + include_hidden, + false, + overwrite_detection, + ) { + Err(err) => { + self.send(fs::new_error(id, err, 0)).await; + } + Ok(job) => { + if check_file_limit { + if let Err(msg) = + crate::ui_cm_interface::check_file_count_limit(job.files().len()) + { + self.send(fs::new_error(id, msg, -1)).await; + return; + } + } + self.process_new_read_job(job, path).await; + } + } + } + #[inline] async fn send(&mut self, msg: Message) { allow_err!(self.stream.send(&msg).await); @@ -4436,6 +4772,23 @@ async fn start_ipc( let data = ipc::Data::ClickTime(ct); stream.send(&data).await?; } + // FileBlockFromCM: data is always sent separately via send_raw. + // The data field has #[serde(skip)], so it's empty after deserialization. + // Read the raw data bytes following this message. + // + // Note: Empty data (for empty files) is correctly handled. BytesCodec with + // raw=false adds a length prefix, so next_raw() returns empty BytesMut for + // zero-length frames. This mirrors the WriteBlock pattern below. + ipc::Data::FileBlockFromCM { id, file_num, data: _, compressed, conn_id } => { + let raw_data = stream.next_raw().await?; + tx_from_cm.send(ipc::Data::FileBlockFromCM { + id, + file_num, + data: raw_data.into(), + compressed, + conn_id, + })?; + } _ => { tx_from_cm.send(data)?; } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index 203651b58..adb6a7a97 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -17,13 +17,12 @@ use rdev::{self, EventType, Key as RdevKey, KeyCode, RawKey}; use rdev::{CGEventSourceStateID, CGEventTapLocation, VirtualInput}; #[cfg(target_os = "linux")] use scrap::wayland::pipewire::RDP_SESSION_INFO; +#[cfg(target_os = "linux")] +use std::sync::mpsc; use std::{ convert::TryFrom, ops::{Deref, DerefMut}, - sync::{ - atomic::{AtomicBool, Ordering}, - mpsc, - }, + sync::atomic::{AtomicBool, Ordering}, thread, time::{self, Duration, Instant}, }; diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 959187cb9..d1c1d21ef 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -6,13 +6,14 @@ use crate::ipc::{self, Data}; use crate::{clipboard::ClipboardSide, ipc::ClipboardNonFile}; #[cfg(target_os = "windows")] use clipboard::ContextSend; +#[cfg(not(any(target_os = "ios")))] +use hbb_common::fs::serialize_transfer_job; #[cfg(not(any(target_os = "android", target_os = "ios")))] use hbb_common::tokio::sync::mpsc::unbounded_channel; use hbb_common::{ - allow_err, - config::Config, - fs::is_write_need_confirmation, - fs::{self, get_string, new_send_confirm, DigestCheckResult}, + allow_err, bail, + config::{keys::OPTION_FILE_TRANSFER_MAX_FILES, Config}, + fs::{self, get_string, is_write_need_confirmation, new_send_confirm, DigestCheckResult}, log, message_proto::*, protobuf::Message as _, @@ -21,16 +22,18 @@ use hbb_common::{ sync::mpsc::{self, UnboundedSender}, task::spawn_blocking, }, + ResultType, }; #[cfg(target_os = "windows")] use hbb_common::{ config::{keys::*, option2bool}, tokio::sync::Mutex as TokioMutex, - ResultType, }; use serde_derive::Serialize; #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] use std::iter::FromIterator; +#[cfg(not(any(target_os = "ios")))] +use std::path::PathBuf; #[cfg(target_os = "windows")] use std::sync::Arc; use std::{ @@ -42,6 +45,85 @@ use std::{ }, }; +/// Default maximum number of files allowed per transfer request. +/// Unit: number of files (not bytes). +#[cfg(not(any(target_os = "ios")))] +const DEFAULT_MAX_VALIDATED_FILES: usize = 10_000; + +/// Maximum number of files allowed in a single file transfer request. +/// +/// This limit prevents excessive I/O and memory usage when dealing with +/// large directories. It applies to: +/// - CM-side read jobs (server to client file transfers on Windows) +/// - `AllFiles` recursive directory listing operations +/// - Connection-side read jobs (non-Windows platforms) +/// +/// Unit: number of files (not bytes). +/// Default: 10,000 files. +/// Configured via: `OPTION_FILE_TRANSFER_MAX_FILES` ("file-transfer-max-files") +#[cfg(not(any(target_os = "ios")))] +static MAX_VALIDATED_FILES: std::sync::OnceLock = std::sync::OnceLock::new(); + +/// Get the maximum number of files allowed per transfer request. +/// +/// Initializes the value from configuration (`OPTION_FILE_TRANSFER_MAX_FILES`) +/// on first call. Semantics: +/// - If the option is set to `0`, `DEFAULT_MAX_VALIDATED_FILES` (10,000) is used as a safe upper bound. +/// - If the option is unset, negative, or non-integer, +/// `usize::MAX` is used to represent "no limit" for backward compatibility with older versions +/// that did not enforce any file‑count restriction. +/// (Note: negative values are not valid for `usize` and will cause parsing to fail.) +/// +/// Unit: number of files. +#[cfg(not(any(target_os = "ios")))] +#[inline] +pub fn get_max_validated_files() -> usize { + // If `OPTION_FILE_TRANSFER_MAX_FILES` unset, negative, or non-integer, use + // `usize::MAX` to represent "no limit", maintaining backward compatibility + // with versions that had no file transfer restrictions. + const NO_LIMIT_FILE_COUNT: usize = usize::MAX; + *MAX_VALIDATED_FILES.get_or_init(|| { + let c = crate::get_builtin_option(OPTION_FILE_TRANSFER_MAX_FILES) + .trim() + .parse::() + .unwrap_or(NO_LIMIT_FILE_COUNT); + if c == 0 { + DEFAULT_MAX_VALIDATED_FILES + } else { + c + } + }) +} + +/// Check if file count exceeds the maximum allowed limit. +/// +/// This check is enforced in: +/// - `start_read_job()` for CM-side read jobs +/// - `read_all_files()` for recursive directory listings +/// - `Connection::on_message()` for connection-side read jobs +/// +/// # Arguments +/// * `file_count` - Number of files in the transfer request +/// +/// # Returns +/// * `Ok(())` if within limit +/// * `Err(String)` with error message if limit exceeded +#[cfg(not(any(target_os = "ios")))] +pub fn check_file_count_limit(file_count: usize) -> Result<(), String> { + let max_files = get_max_validated_files(); + if file_count > max_files { + let msg = format!( + "file transfer rejected: too many files ({} files exceeds limit of {}). \ + Adjust '{}' option to increase limit.", + file_count, max_files, OPTION_FILE_TRANSFER_MAX_FILES + ); + log::warn!("{}", msg); + Err(msg) + } else { + Ok(()) + } +} + #[derive(Serialize, Clone)] pub struct Client { pub id: i32, @@ -81,6 +163,8 @@ struct IpcTaskRunner { file_transfer_enabled: bool, #[cfg(target_os = "windows")] file_transfer_enabled_peer: bool, + /// Read jobs for CM-side file reading (server to client transfers) + read_jobs: Vec, } lazy_static::lazy_static! { @@ -348,9 +432,16 @@ pub fn switch_back(id: i32) { impl IpcTaskRunner { async fn run(&mut self) { use hbb_common::config::LocalConfig; + use hbb_common::tokio::time::{self, Duration, Instant}; + + const MILLI5: Duration = Duration::from_millis(5); + const SEC30: Duration = Duration::from_secs(30); // for tmp use, without real conn id let mut write_jobs: Vec = Vec::new(); + // File timer for processing read_jobs + let mut file_timer = + crate::rustdesk_interval(time::interval_at(Instant::now() + SEC30, SEC30)); #[cfg(target_os = "windows")] let is_authorized = self.cm.is_authorized(self.conn_id); @@ -443,10 +534,16 @@ impl IpcTaskRunner { if let ipc::FS::WriteBlock { id, file_num, data: _, compressed } = fs { if let Ok(bytes) = self.stream.next_raw().await { fs = ipc::FS::WriteBlock{id, file_num, data:bytes.into(), compressed}; - handle_fs(fs, &mut write_jobs, &self.tx, Some(&tx_log)).await; + handle_fs(fs, &mut write_jobs, &mut self.read_jobs, &self.tx, Some(&tx_log), self.conn_id).await; } } else { - handle_fs(fs, &mut write_jobs, &self.tx, Some(&tx_log)).await; + handle_fs(fs, &mut write_jobs, &mut self.read_jobs, &self.tx, Some(&tx_log), self.conn_id).await; + } + // Activate fast timer immediately when read jobs exist. + // This ensures new jobs start processing without waiting for the slow 30s timer. + // Deactivation (back to 30s) happens in tick handler when jobs are exhausted. + if !self.read_jobs.is_empty() { + file_timer = crate::rustdesk_interval(time::interval(MILLI5)); } let log = fs::serialize_transfer_jobs(&write_jobs); self.cm.ui_handler.file_transfer_log("transfer", &log); @@ -550,6 +647,31 @@ impl IpcTaskRunner { } } Some(data) = self.rx.recv() => { + // For FileBlockFromCM, data is sent separately via send_raw (data field has #[serde(skip)]). + // This avoids JSON encoding overhead for large binary data. + // This mirrors the WriteBlock pattern in start_ipc (see rx_to_cm handler). + // + // Note: Empty data (for empty files) is correctly handled. BytesCodec with raw=false + // (the default for IPC connections) adds a length prefix, so send_raw(Bytes::new()) + // sends a 1-byte frame that next_raw() can correctly receive as empty data. + if let Data::FileBlockFromCM { id, file_num, ref data, compressed, conn_id } = data { + // Send metadata first (data field is skipped by serde), then raw data bytes + if let Err(e) = self.stream.send(&Data::FileBlockFromCM { + id, + file_num, + data: bytes::Bytes::new(), // placeholder, skipped by serde + compressed, + conn_id, + }).await { + log::error!("error sending FileBlockFromCM metadata: {}", e); + break; + } + if let Err(e) = self.stream.send_raw(data.clone()).await { + log::error!("error sending FileBlockFromCM data: {}", e); + break; + } + continue; + } if let Err(e) = self.stream.send(&data).await { log::error!("error encountered in IPC task, quitting: {}", e); break; @@ -600,6 +722,18 @@ impl IpcTaskRunner { Some(job_log) = rx_log.recv() => { self.cm.ui_handler.file_transfer_log("transfer", &job_log); } + _ = file_timer.tick() => { + if !self.read_jobs.is_empty() { + let conn_id = self.conn_id; + if let Err(e) = handle_read_jobs_tick(&mut self.read_jobs, &self.tx, conn_id).await { + log::error!("Error processing read jobs: {}", e); + } + let log = fs::serialize_transfer_jobs(&self.read_jobs); + self.cm.ui_handler.file_transfer_log("transfer", &log); + } else { + file_timer = crate::rustdesk_interval(time::interval_at(Instant::now() + SEC30, SEC30)); + } + } } } } @@ -619,6 +753,7 @@ impl IpcTaskRunner { file_transfer_enabled: false, #[cfg(target_os = "windows")] file_transfer_enabled_peer: false, + read_jobs: Vec::new(), }; while task_runner.running { @@ -720,7 +855,17 @@ pub async fn start_listen( cm.new_message(current_id, text); } Some(Data::FS(fs)) => { - handle_fs(fs, &mut write_jobs, &tx, None).await; + // Android doesn't need CM-side file reading (no need_validate_file_read_access) + let mut read_jobs_placeholder: Vec = Vec::new(); + handle_fs( + fs, + &mut write_jobs, + &mut read_jobs_placeholder, + &tx, + None, + current_id, + ) + .await; } Some(Data::Close) => { break; @@ -747,13 +892,11 @@ pub async fn start_listen( async fn handle_fs( fs: ipc::FS, write_jobs: &mut Vec, + read_jobs: &mut Vec, tx: &UnboundedSender, tx_log: Option<&UnboundedSender>, + _conn_id: i32, ) { - use std::path::PathBuf; - - use hbb_common::fs::serialize_transfer_job; - match fs { ipc::FS::ReadEmptyDirs { dir, @@ -789,6 +932,25 @@ async fn handle_fs( total_size, conn_id, } => { + // Validate file names to prevent path traversal attacks. + // This must be done BEFORE any path operations to ensure attackers cannot + // escape the target directory using names like "../../malicious.txt" + if let Err(e) = validate_transfer_file_names(&files) { + log::warn!("Path traversal attempt detected for {}: {}", path, e); + send_raw(fs::new_error(id, e, file_num), tx); + return; + } + + // Convert files to FileEntry + let file_entries: Vec = files + .drain(..) + .map(|f| FileEntry { + name: f.0, + modified_time: f.1, + ..Default::default() + }) + .collect(); + // cm has no show_hidden context // dummy remote, show_hidden, is_remote let mut job = fs::TransferJob::new_write( @@ -799,14 +961,7 @@ async fn handle_fs( file_num, false, false, - files - .drain(..) - .map(|f| FileEntry { - name: f.0, - modified_time: f.1, - ..Default::default() - }) - .collect(), + file_entries, overwrite_detection, ); job.total_size = total_size; @@ -816,9 +971,11 @@ async fn handle_fs( ipc::FS::CancelWrite { id } => { if let Some(job) = fs::remove_job(id, write_jobs) { job.remove_download_file(); - tx_log.map(|tx: &UnboundedSender| { - tx.send(serialize_transfer_job(&job, false, true, "")) - }); + if let Some(tx) = tx_log { + if let Err(e) = tx.send(serialize_transfer_job(&job, false, true, "")) { + log::error!("error sending transfer job log via IPC: {}", e); + } + } } } ipc::FS::WriteDone { id, file_num } => { @@ -922,10 +1079,436 @@ async fn handle_fs( ipc::FS::Rename { id, path, new_name } => { rename_file(path, new_name, id, tx).await; } + ipc::FS::ReadFile { + path, + id, + file_num, + include_hidden, + conn_id, + overwrite_detection, + } => { + start_read_job( + path, + file_num, + include_hidden, + id, + conn_id, + overwrite_detection, + read_jobs, + tx, + ) + .await; + } + // Cancel an ongoing read job (file transfer from server to client). + // Note: This only cancels jobs in `read_jobs`. It does NOT cancel `ReadAllFiles` + // operations, which are one-shot directory scans that complete quickly and don't + // have persistent job tracking. + ipc::FS::CancelRead { id, conn_id: _ } => { + if let Some(job) = fs::remove_job(id, read_jobs) { + if let Some(tx) = tx_log { + if let Err(e) = tx.send(serialize_transfer_job(&job, false, true, "")) { + log::error!("error sending transfer job log via IPC: {}", e); + } + } + } + } + ipc::FS::SendConfirmForRead { + id, + file_num: _, + skip, + offset_blk, + conn_id: _, + } => { + if let Some(job) = fs::get_job(id, read_jobs) { + let req = FileTransferSendConfirmRequest { + id, + file_num: job.file_num(), + union: if skip { + Some(file_transfer_send_confirm_request::Union::Skip(true)) + } else { + Some(file_transfer_send_confirm_request::Union::OffsetBlk( + offset_blk, + )) + }, + ..Default::default() + }; + job.confirm(&req).await; + } + } + // Recursively list all files in a directory. + // This is a one-shot operation that cannot be cancelled via CancelRead. + // The operation typically completes quickly as it only reads directory metadata, + // not file contents. File count is limited by `check_file_count_limit()`. + ipc::FS::ReadAllFiles { + path, + id, + include_hidden, + conn_id, + } => { + read_all_files(path, include_hidden, id, conn_id, tx).await; + } _ => {} } } +/// Validates that a file name does not contain path traversal sequences. +/// This prevents attackers from escaping the base directory by using names like +/// "../../../etc/passwd" or "..\\..\\Windows\\System32\\malicious.dll". +#[cfg(not(any(target_os = "ios")))] +fn validate_file_name_no_traversal(name: &str) -> ResultType<()> { + // Check for null bytes which could cause path truncation in some APIs + if name.bytes().any(|b| b == 0) { + bail!("file name contains null bytes"); + } + + // Check for path traversal patterns + // We check for both Unix and Windows path separators + if name + .split(|c| c == '/' || c == '\\') + .filter(|s| !s.is_empty()) + .any(|component| component == "..") + { + bail!("path traversal detected in file name"); + } + + // On Windows, also check for drive letters (e.g., "C:") + #[cfg(windows)] + { + if name.len() >= 2 { + let bytes = name.as_bytes(); + if bytes[0].is_ascii_alphabetic() && bytes[1] == b':' { + bail!("absolute path detected in file name"); + } + } + } + + // Check for names starting with path separator: + // - Unix absolute paths (e.g., "/etc/passwd") + // - Windows UNC paths (e.g., "\\server\share") + if name.starts_with('/') || name.starts_with('\\') { + bail!("absolute path detected in file name"); + } + + Ok(()) +} + +#[inline] +fn is_single_file_with_empty_name(files: &[(String, u64)]) -> bool { + files.len() == 1 && files.first().map_or(false, |f| f.0.is_empty()) +} + +/// Validates all file names in a transfer request to prevent path traversal attacks. +/// Returns an error if any file name contains dangerous path components. +#[cfg(not(any(target_os = "ios")))] +fn validate_transfer_file_names(files: &[(String, u64)]) -> ResultType<()> { + if is_single_file_with_empty_name(files) { + // Allow empty name for single file. + // The full path is provided in the `path` parameter for single file transfers. + return Ok(()); + } + + for (name, _) in files { + // In multi-file transfers, empty names are not allowed. + // Each file must have a valid name to construct the destination path. + if name.is_empty() { + bail!("empty file name in multi-file transfer"); + } + validate_file_name_no_traversal(name)?; + } + Ok(()) +} + +/// Start a read job in CM for file transfer from server to client (Windows only). +/// +/// This creates a `TransferJob` using `new_read()`, validates it, and sends the +/// initial file list back to Connection via IPC. +/// +/// NOTE: This is the CM-side equivalent of `create_and_start_read_job()` in +/// `src/server/connection.rs`. On non-Windows platforms, Connection handles +/// read jobs directly. Both use `TransferJob::new_read()` with similar logic. +/// When modifying job creation or validation, ensure both paths stay in sync. +#[cfg(not(any(target_os = "ios")))] +async fn start_read_job( + path: String, + file_num: i32, + include_hidden: bool, + id: i32, + conn_id: i32, + overwrite_detection: bool, + read_jobs: &mut Vec, + tx: &UnboundedSender, +) { + let path_clone = path.clone(); + let result = spawn_blocking(move || -> ResultType { + let data_source = fs::DataSource::FilePath(PathBuf::from(&path)); + fs::TransferJob::new_read( + id, + fs::JobType::Generic, + "".to_string(), + data_source, + file_num, + include_hidden, + true, + overwrite_detection, + ) + }) + .await; + + match result { + Ok(Ok(mut job)) => { + // Optional: enforce file count limit for CM-side jobs to avoid + // excessive I/O. This is applied on the job's file list produced + // by `new_read`, similar to how AllFiles uses the same helper. + if let Err(msg) = check_file_count_limit(job.files().len()) { + if let Err(e) = tx.send(Data::ReadJobInitResult { + id, + file_num, + include_hidden, + conn_id, + result: Err(msg), + }) { + log::error!("error sending ReadJobInitResult via IPC: {}", e); + } + return; + } + + // Build FileDirectory from the job's file list and serialize + let files = job.files().to_owned(); + let mut dir = FileDirectory::new(); + dir.id = id; + dir.path = path_clone.clone(); + dir.entries = files.clone().into(); + + let dir_bytes = match dir.write_to_bytes() { + Ok(bytes) => bytes, + Err(e) => { + if let Err(e) = tx.send(Data::ReadJobInitResult { + id, + file_num, + include_hidden, + conn_id, + result: Err(format!("serialize failed: {}", e)), + }) { + log::error!("error sending ReadJobInitResult via IPC: {}", e); + } + return; + } + }; + + if let Err(e) = tx.send(Data::ReadJobInitResult { + id, + file_num, + include_hidden, + conn_id, + result: Ok(dir_bytes), + }) { + log::error!("error sending ReadJobInitResult via IPC: {}", e); + } + + // Attach connection id so CM can route read blocks back correctly + job.conn_id = conn_id; + read_jobs.push(job); + } + Ok(Err(e)) => { + if let Err(e) = tx.send(Data::ReadJobInitResult { + id, + file_num, + include_hidden, + conn_id, + result: Err(format!("validation failed: {}", e)), + }) { + log::error!("error sending ReadJobInitResult via IPC: {}", e); + } + } + Err(e) => { + if let Err(e) = tx.send(Data::ReadJobInitResult { + id, + file_num, + include_hidden, + conn_id, + result: Err(format!("validation task failed: {}", e)), + }) { + log::error!("error sending ReadJobInitResult via IPC: {}", e); + } + } + } +} + +/// Process read jobs periodically, reading file blocks and sending them via IPC. +/// +/// NOTE: This is the CM-side equivalent of `handle_read_jobs()` in +/// `libs/hbb_common/src/fs.rs`. The logic mirrors that implementation +/// but communicates via IPC instead of direct network stream. +/// When modifying job processing logic, ensure both implementations stay in sync. +#[cfg(not(any(target_os = "ios")))] +async fn handle_read_jobs_tick( + jobs: &mut Vec, + tx: &UnboundedSender, + conn_id: i32, +) -> ResultType<()> { + let mut finished = Vec::new(); + + for job in jobs.iter_mut() { + if job.is_last_job { + continue; + } + + // Initialize data stream if needed (opens file, sends digest for overwrite detection) + if let Err(err) = init_read_job_for_cm(job, tx, conn_id).await { + if let Err(e) = tx.send(Data::FileReadError { + id: job.id, + file_num: job.file_num(), + err: format!("{}", err), + conn_id, + }) { + log::error!("error sending FileReadError via IPC: {}", e); + } + finished.push(job.id); + continue; + } + + // Read a block from the file + match job.read().await { + Err(err) => { + if let Err(e) = tx.send(Data::FileReadError { + id: job.id, + file_num: job.file_num(), + err: format!("{}", err), + conn_id, + }) { + log::error!("error sending FileReadError via IPC: {}", e); + } + // Mark job as finished to prevent infinite retries. + // Connection side will have already removed cm_read_job_ids + // after receiving FileReadError, so continuing would be pointless. + finished.push(job.id); + } + Ok(Some(block)) => { + if let Err(e) = tx.send(Data::FileBlockFromCM { + id: block.id, + file_num: block.file_num, + data: block.data, + compressed: block.compressed, + conn_id, + }) { + log::error!("error sending FileBlockFromCM via IPC: {}", e); + } + } + Ok(None) => { + if job.job_completed() { + finished.push(job.id); + match job.job_error() { + Some(err) => { + if let Err(e) = tx.send(Data::FileReadError { + id: job.id, + file_num: job.file_num(), + err, + conn_id, + }) { + log::error!("error sending FileReadError via IPC: {}", e); + } + } + None => { + if let Err(e) = tx.send(Data::FileReadDone { + id: job.id, + file_num: job.file_num(), + conn_id, + }) { + log::error!("error sending FileReadDone via IPC: {}", e); + } + } + } + } + // else: waiting for confirmation from peer + } + } + // Break to handle jobs one by one. + break; + } + + for id in finished { + let _ = fs::remove_job(id, jobs); + } + + Ok(()) +} + +/// Initialize a read job's data stream and handle digest sending for overwrite detection. +/// +/// NOTE: This is the CM-side equivalent of `TransferJob::init_data_stream()` in +/// `libs/hbb_common/src/fs.rs`. It calls `init_data_stream_for_cm()` and sends +/// digest via IPC instead of direct network stream. +/// When modifying initialization or digest logic, ensure both paths stay in sync. +#[cfg(not(any(target_os = "ios")))] +async fn init_read_job_for_cm( + job: &mut fs::TransferJob, + tx: &UnboundedSender, + conn_id: i32, +) -> ResultType<()> { + // Initialize data stream and get digest info if overwrite detection is needed + match job.init_data_stream_for_cm().await? { + Some((last_modified, file_size)) => { + // Send digest via IPC for overwrite detection + if let Err(e) = tx.send(Data::FileDigestFromCM { + id: job.id, + file_num: job.file_num(), + last_modified, + file_size, + is_resume: job.is_resume, + conn_id, + }) { + log::error!("error sending FileDigestFromCM via IPC: {}", e); + } + } + None => { + // Job done or already initialized, nothing to do + } + } + Ok(()) +} + +#[cfg(not(any(target_os = "ios")))] +async fn read_all_files( + path: String, + include_hidden: bool, + id: i32, + conn_id: i32, + tx: &UnboundedSender, +) { + let path_clone = path.clone(); + let result = spawn_blocking(move || fs::get_recursive_files(&path, include_hidden)).await; + + let result = match result { + Ok(Ok(files)) => { + // Check file count limit to prevent excessive I/O and resource usage + if let Err(msg) = check_file_count_limit(files.len()) { + Err(msg) + } else { + // Serialize FileDirectory to protobuf bytes + let mut fd = FileDirectory::new(); + fd.id = id; + fd.path = path_clone.clone(); + fd.entries = files.into(); + match fd.write_to_bytes() { + Ok(bytes) => Ok(bytes), + Err(e) => Err(format!("serialize failed: {}", e)), + } + } + } + Ok(Err(e)) => Err(format!("{}", e)), + Err(e) => Err(format!("task failed: {}", e)), + }; + + if let Err(e) = tx.send(Data::AllFilesResult { + id, + conn_id, + path: path_clone, + result, + }) { + log::error!("error sending AllFilesResult via IPC: {}", e); + } +} + #[cfg(not(any(target_os = "ios")))] async fn read_empty_dirs(dir: &str, include_hidden: bool, tx: &UnboundedSender) { let path = dir.to_owned(); @@ -1009,7 +1592,16 @@ async fn create_dir(path: String, id: i32, tx: &UnboundedSender) { #[cfg(not(any(target_os = "ios")))] async fn rename_file(path: String, new_name: String, id: i32, tx: &UnboundedSender) { handle_result( - spawn_blocking(move || fs::rename_file(&path, &new_name)).await, + spawn_blocking(move || { + // Rename target must not be empty + if new_name.is_empty() { + bail!("new file name cannot be empty"); + } + // Validate that new_name doesn't contain path traversal + validate_file_name_no_traversal(&new_name)?; + fs::rename_file(&path, &new_name) + }) + .await, id, 0, tx, @@ -1106,3 +1698,147 @@ pub fn quit_cm() { CLIENTS.write().unwrap().clear(); crate::platform::quit_gui(); } + +#[cfg(test)] +mod tests { + use super::*; + + use crate::ipc::Data; + use hbb_common::{ + message_proto::{FileDirectory, Message}, + tokio::{runtime::Runtime, sync::mpsc::unbounded_channel}, + }; + use std::fs; + + #[test] + #[cfg(not(any(target_os = "ios")))] + fn read_all_files_success() { + let rt = Runtime::new().unwrap(); + rt.block_on(async { + let (tx, mut rx) = unbounded_channel(); + let dir = std::env::temp_dir().join("rustdesk_read_all_test"); + let _ = fs::remove_dir_all(&dir); + fs::create_dir_all(&dir).unwrap(); + fs::write(dir.join("test.txt"), b"hello").unwrap(); + + let path_str = dir.to_string_lossy().to_string(); + super::read_all_files(path_str.clone(), false, 1, 2, &tx).await; + + match rx.recv().await.unwrap() { + Data::AllFilesResult { result, .. } => { + let bytes = result.unwrap(); + let fd = FileDirectory::parse_from_bytes(&bytes).unwrap(); + assert!(!fd.entries.is_empty()); + } + _ => panic!("unexpected data"), + } + let _ = fs::remove_dir_all(&dir); + }); + } + + #[test] + #[cfg(not(any(target_os = "ios")))] + fn read_dir_success() { + let rt = Runtime::new().unwrap(); + rt.block_on(async { + let (tx, mut rx) = unbounded_channel(); + let dir = std::env::temp_dir().join("rustdesk_read_dir_test"); + let _ = fs::remove_dir_all(&dir); + fs::create_dir_all(&dir).unwrap(); + + super::read_dir(&dir.to_string_lossy(), false, &tx).await; + + match rx.recv().await.unwrap() { + Data::RawMessage(bytes) => { + let mut msg = Message::new(); + msg.merge_from_bytes(&bytes).unwrap(); + assert!(msg + .file_response() + .dir() + .path + .contains("rustdesk_read_dir_test")); + } + _ => panic!("unexpected data"), + } + let _ = fs::remove_dir_all(&dir); + }); + } + + #[test] + #[cfg(not(any(target_os = "ios")))] + fn validate_file_name_security() { + // Null byte injection + assert!(super::validate_file_name_no_traversal("file\0.txt").is_err()); + assert!(super::validate_file_name_no_traversal("test\0").is_err()); + + // Path traversal + assert!(super::validate_file_name_no_traversal("../etc/passwd").is_err()); + assert!(super::validate_file_name_no_traversal("foo/../bar").is_err()); + assert!(super::validate_file_name_no_traversal("..").is_err()); + + // Absolute paths + assert!(super::validate_file_name_no_traversal("/etc/passwd").is_err()); + assert!(super::validate_file_name_no_traversal("\\Windows").is_err()); + #[cfg(windows)] + assert!(super::validate_file_name_no_traversal("C:\\Windows").is_err()); + + // Valid paths + assert!(super::validate_file_name_no_traversal("file.txt").is_ok()); + assert!(super::validate_file_name_no_traversal("subdir/file.txt").is_ok()); + assert!(super::validate_file_name_no_traversal("").is_ok()); + } + + #[test] + #[cfg(not(any(target_os = "ios")))] + fn validate_transfer_file_names_security() { + assert!(super::validate_transfer_file_names(&[("file.txt".into(), 100)]).is_ok()); + assert!(super::validate_transfer_file_names(&[("".into(), 100)]).is_ok()); + assert!( + super::validate_transfer_file_names(&[("".into(), 100), ("file.txt".into(), 100)]) + .is_err() + ); + assert!(super::validate_transfer_file_names(&[("../passwd".into(), 100)]).is_err()); + } + + /// Tests that symlink creation works on this platform. + /// This is a helper to verify the test environment supports symlinks. + #[test] + #[cfg(not(any(target_os = "ios")))] + fn test_symlink_creation_works() { + let base_dir = std::env::temp_dir().join("rustdesk_symlink_test"); + let _ = fs::remove_dir_all(&base_dir); + fs::create_dir_all(&base_dir).unwrap(); + + // Create target file in a subdirectory + let target_dir = base_dir.join("target_dir"); + fs::create_dir_all(&target_dir).unwrap(); + let target_file = target_dir.join("target.txt"); + fs::write(&target_file, b"content").unwrap(); + + // Create symlink in a different directory + let link_dir = base_dir.join("link_dir"); + fs::create_dir_all(&link_dir).unwrap(); + let link_path = link_dir.join("link.txt"); + + #[cfg(unix)] + { + use std::os::unix::fs::symlink; + if symlink(&target_file, &link_path).is_err() { + let _ = fs::remove_dir_all(&base_dir); + return; + } + } + + #[cfg(windows)] + { + use std::os::windows::fs::symlink_file; + if symlink_file(&target_file, &link_path).is_err() { + // Skip if no permission (needs admin or dev mode on Windows) + let _ = fs::remove_dir_all(&base_dir); + return; + } + } + + let _ = fs::remove_dir_all(&base_dir); + } +} From 3384eda8b73a1b8f78f2c7e2d23b76585dd4c34e Mon Sep 17 00:00:00 2001 From: alonginwind <100897495+alonginwind@users.noreply.github.com> Date: Sun, 28 Dec 2025 15:41:25 +0800 Subject: [PATCH 047/277] feat(terminal): add two-row floating keyboard buttons for common commands (mobile only) (#13876) * feat(terminal): add two-row floating keyboard buttons for common commands (mobile only) * Fix missing newline at end of pl.rs Add missing newline at the end of the file. --- flutter/lib/consts.dart | 1 + flutter/lib/mobile/pages/settings_page.dart | 20 ++ flutter/lib/mobile/pages/terminal_page.dart | 205 +++++++++++++++++--- flutter/lib/models/terminal_model.dart | 4 + src/lang/ar.rs | 1 + src/lang/be.rs | 1 + src/lang/bg.rs | 1 + src/lang/ca.rs | 1 + src/lang/cn.rs | 1 + src/lang/cs.rs | 1 + src/lang/da.rs | 1 + src/lang/de.rs | 1 + src/lang/el.rs | 1 + src/lang/eo.rs | 1 + src/lang/es.rs | 1 + src/lang/et.rs | 1 + src/lang/eu.rs | 1 + src/lang/fa.rs | 1 + src/lang/fi.rs | 1 + src/lang/fr.rs | 1 + src/lang/ge.rs | 1 + src/lang/he.rs | 1 + src/lang/hr.rs | 1 + src/lang/hu.rs | 1 + src/lang/id.rs | 1 + src/lang/it.rs | 1 + src/lang/ja.rs | 1 + src/lang/ko.rs | 1 + src/lang/kz.rs | 1 + src/lang/lt.rs | 1 + src/lang/lv.rs | 1 + src/lang/nb.rs | 1 + src/lang/nl.rs | 1 + src/lang/pl.rs | 1 + src/lang/pt_PT.rs | 1 + src/lang/ptbr.rs | 1 + src/lang/ro.rs | 1 + src/lang/ru.rs | 1 + src/lang/sc.rs | 1 + src/lang/sk.rs | 1 + src/lang/sl.rs | 1 + src/lang/sq.rs | 1 + src/lang/sr.rs | 1 + src/lang/sv.rs | 1 + src/lang/ta.rs | 1 + src/lang/template.rs | 1 + src/lang/th.rs | 1 + src/lang/tr.rs | 1 + src/lang/tw.rs | 1 + src/lang/uk.rs | 1 + src/lang/vi.rs | 1 + 51 files changed, 247 insertions(+), 30 deletions(-) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 94a0aaac5..7bcadd658 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -163,6 +163,7 @@ const String kOptionShowVirtualMouse = "show-virtual-mouse"; const String kOptionVirtualMouseScale = "virtual-mouse-scale"; const String kOptionShowVirtualJoystick = "show-virtual-joystick"; const String kOptionAllowAskForNoteAtEndOfConnection = "allow-ask-for-note"; +const String kOptionEnableShowTerminalExtraKeys = "enable-show-terminal-extra-keys"; // network options const String kOptionAllowWebSocket = "allow-websocket"; diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 69a9d6a44..afe8ae446 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -71,6 +71,7 @@ class _SettingsState extends State with WidgetsBindingObserver { var _ignoreBatteryOpt = false; var _enableStartOnBoot = false; var _checkUpdateOnStartup = false; + var _showTerminalExtraKeys = false; var _floatingWindowDisabled = false; var _keepScreenOn = KeepScreenOn.duringControlled; // relay on floating window var _enableAbr = false; @@ -139,6 +140,8 @@ class _SettingsState extends State with WidgetsBindingObserver { _enableIpv6Punch = mainGetLocalBoolOptionSync(kOptionEnableIpv6Punch); _allowAskForNoteAtEndOfConnection = mainGetLocalBoolOptionSync(kOptionAllowAskForNoteAtEndOfConnection); + _showTerminalExtraKeys = + mainGetLocalBoolOptionSync(kOptionEnableShowTerminalExtraKeys); } @override @@ -602,6 +605,23 @@ class _SettingsState extends State with WidgetsBindingObserver { ); } + enhancementsTiles.add( + SettingsTile.switchTile( + initialValue: _showTerminalExtraKeys, + title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(translate('Show terminal extra keys')), + ]), + onToggle: (bool v) async { + await mainSetLocalBoolOption(kOptionEnableShowTerminalExtraKeys, v); + final newValue = + mainGetLocalBoolOptionSync(kOptionEnableShowTerminalExtraKeys); + setState(() { + _showTerminalExtraKeys = newValue; + }); + }, + ), + ); + onFloatingWindowChanged(bool toValue) async { if (toValue) { if (!await AndroidPermissionManager.check(kSystemAlertWindow)) { diff --git a/flutter/lib/mobile/pages/terminal_page.dart b/flutter/lib/mobile/pages/terminal_page.dart index 35dcb04bd..a0064f068 100644 --- a/flutter/lib/mobile/pages/terminal_page.dart +++ b/flutter/lib/mobile/pages/terminal_page.dart @@ -9,6 +9,7 @@ import 'package:flutter_hbb/models/terminal_model.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:xterm/xterm.dart'; import '../../desktop/pages/terminal_connection_manager.dart'; +import '../../consts.dart'; class TerminalPage extends StatefulWidget { const TerminalPage({ @@ -37,6 +38,9 @@ class _TerminalPageState extends State double? _cellHeight; double _sysKeyboardHeight = 0; Timer? _keyboardDebounce; + final GlobalKey _keyboardKey = GlobalKey(); + double _keyboardHeight = 0; + late bool _showTerminalExtraKeys; // For web only. // 'monospace' does not work on web, use Google Fonts, `??` is only for null safety. @@ -75,10 +79,15 @@ class _TerminalPageState extends State // Register this terminal model with FFI for event routing _ffi.registerTerminalModel(widget.terminalId, _terminalModel); + _showTerminalExtraKeys = mainGetLocalBoolOptionSync(kOptionEnableShowTerminalExtraKeys); // Initialize terminal connection WidgetsBinding.instance.addPostFrameCallback((_) { _ffi.dialogManager .showLoading(translate('Connecting...'), onCancel: closeConnection); + + if (_showTerminalExtraKeys) { + _updateKeyboardHeight(); + } }); _ffi.ffiModel.updateEventListener(_ffi.sessionId, widget.id); } @@ -107,15 +116,22 @@ class _TerminalPageState extends State }); } + void _updateKeyboardHeight() { + if (_keyboardKey.currentContext != null) { + final renderBox = _keyboardKey.currentContext!.findRenderObject() as RenderBox; + _keyboardHeight = renderBox.size.height; + } + } + EdgeInsets _calculatePadding(double heightPx) { if (_cellHeight == null) { return const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0); } - final realHeight = heightPx - _sysKeyboardHeight; + final realHeight = heightPx - _sysKeyboardHeight - _keyboardHeight; final rows = (realHeight / _cellHeight!).floor(); final extraSpace = realHeight - rows * _cellHeight!; final topBottom = max(0.0, extraSpace / 2.0); - return EdgeInsets.only(left: 5.0, right: 5.0, top: topBottom, bottom: topBottom + _sysKeyboardHeight); + return EdgeInsets.only(left: 5.0, right: 5.0, top: topBottom, bottom: topBottom + _sysKeyboardHeight + _keyboardHeight); } @override @@ -134,39 +150,168 @@ class _TerminalPageState extends State return Scaffold( resizeToAvoidBottomInset: false, // Disable automatic layout adjustment; manually control UI updates to prevent flickering when the keyboard shows/hides backgroundColor: Theme.of(context).scaffoldBackgroundColor, - body: SafeArea( - top: true, - child: LayoutBuilder( - builder: (context, constraints) { - final heightPx = constraints.maxHeight; - return TerminalView( - _terminalModel.terminal, - controller: _terminalModel.terminalController, - autofocus: true, - textStyle: _getTerminalStyle(), - backgroundOpacity: 0.7, - padding: _calculatePadding(heightPx), - onSecondaryTapDown: (details, offset) async { - final selection = _terminalModel.terminalController.selection; - if (selection != null) { - final text = _terminalModel.terminal.buffer.getText(selection); - _terminalModel.terminalController.clearSelection(); - await Clipboard.setData(ClipboardData(text: text)); - } else { - final data = await Clipboard.getData('text/plain'); - final text = data?.text; - if (text != null) { - _terminalModel.terminal.paste(text); - } - } - }, - ); - }, + body: Stack( + children: [ + Positioned.fill( + child: SafeArea( + top: true, + child: LayoutBuilder( + builder: (context, constraints) { + final heightPx = constraints.maxHeight; + return TerminalView( + _terminalModel.terminal, + controller: _terminalModel.terminalController, + autofocus: true, + textStyle: _getTerminalStyle(), + backgroundOpacity: 0.7, + padding: _calculatePadding(heightPx), + onSecondaryTapDown: (details, offset) async { + final selection = _terminalModel.terminalController.selection; + if (selection != null) { + final text = _terminalModel.terminal.buffer.getText(selection); + _terminalModel.terminalController.clearSelection(); + await Clipboard.setData(ClipboardData(text: text)); + } else { + final data = await Clipboard.getData('text/plain'); + final text = data?.text; + if (text != null) { + _terminalModel.terminal.paste(text); + } + } + }, + ); + }, + ), + ), + ), + if (_showTerminalExtraKeys) _buildFloatingKeyboard(), + ], + ), + ); + } + + Widget _buildFloatingKeyboard() { + return AnimatedPositioned( + duration: const Duration(milliseconds: 200), + left: 0, + right: 0, + bottom: _sysKeyboardHeight, + child: Container( + key: _keyboardKey, + color: Theme.of(context).scaffoldBackgroundColor, + padding: EdgeInsets.zero, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildKeyButton('Esc'), + const SizedBox(width: 2), + _buildKeyButton('/'), + const SizedBox(width: 2), + _buildKeyButton('|'), + const SizedBox(width: 2), + _buildKeyButton('Home'), + const SizedBox(width: 2), + _buildKeyButton('↑'), + const SizedBox(width: 2), + _buildKeyButton('End'), + const SizedBox(width: 2), + _buildKeyButton('PgUp'), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildKeyButton('Tab'), + const SizedBox(width: 2), + _buildKeyButton('Ctrl+C'), + const SizedBox(width: 2), + _buildKeyButton('~'), + const SizedBox(width: 2), + _buildKeyButton('←'), + const SizedBox(width: 2), + _buildKeyButton('↓'), + const SizedBox(width: 2), + _buildKeyButton('→'), + const SizedBox(width: 2), + _buildKeyButton('PgDn'), + ], + ), + ], ), ), ); } + Widget _buildKeyButton(String label) { + return ElevatedButton( + onPressed: () { + _sendKeyToTerminal(label); + }, + child: Text(label), + style: ElevatedButton.styleFrom( + minimumSize: const Size(48, 32), + padding: EdgeInsets.zero, + textStyle: const TextStyle(fontSize: 12), + backgroundColor: Theme.of(context).colorScheme.surfaceVariant, + foregroundColor: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ); + } + + void _sendKeyToTerminal(String key) { + String? send; + + switch (key) { + case 'Esc': + send = '\x1B'; + break; + case 'Tab': + send = '\t'; + break; + case 'Ctrl+C': + send = '\x03'; + break; + + case '↑': + send = '\x1B[A'; + break; + case '↓': + send = '\x1B[B'; + break; + case '→': + send = '\x1B[C'; + break; + case '←': + send = '\x1B[D'; + break; + + case 'Home': + send = '\x1B[H'; + break; + case 'End': + send = '\x1B[F'; + break; + case 'PgUp': + send = '\x1B[5~'; + break; + case 'PgDn': + send = '\x1B[6~'; + break; + + default: + send = key; + break; + } + + if (send != null) { + _terminalModel.sendVirtualKey(send); + } + } + // https://github.com/TerminalStudio/xterm.dart/issues/42#issuecomment-877495472 // https://github.com/TerminalStudio/xterm.dart/issues/198#issuecomment-2526548458 TerminalStyle _getTerminalStyle() { diff --git a/flutter/lib/models/terminal_model.dart b/flutter/lib/models/terminal_model.dart index b32be65f1..ca4f2c11d 100644 --- a/flutter/lib/models/terminal_model.dart +++ b/flutter/lib/models/terminal_model.dart @@ -146,6 +146,10 @@ class TerminalModel with ChangeNotifier { } } + Future sendVirtualKey(String data) async { + return _handleInput(data); + } + Future closeTerminal() async { if (_terminalOpened) { try { diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 3e5b9ce2d..93ba2987e 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", "هذه الميزة غير مدعومة من قبل خادمك"), ("input note here", "أدخل الملاحظة هنا"), ("note-at-conn-end-tip", "سيتم عرض هذه الملاحظة عند نهاية الاتصال"), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index b7d9bb070..03e833701 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 714a3e0e3..d88f3745f 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 794bd5908..60ccbcbd8 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index db03e2fbc..a125a9f41 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", "注意:RustDesk 开源服务器 (OSS server) 不包含此功能。"), ("input note here", "输入备注"), ("note-at-conn-end-tip", "在连接结束时请求备注"), + ("Show terminal extra keys", "显示终端扩展键"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index ae5b4ef4b..7600f5f54 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index a812698eb..2898629fe 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index caa4c5245..f734d49b9 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", "HINWEIS: RustDesk Server OSS enthält diese Funktion nicht."), ("input note here", "Hier eine Notiz eingeben"), ("note-at-conn-end-tip", "Am Ende der Verbindung um eine Notiz bitten."), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 0d74e0b45..fb51a8001 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index d817e67f5..bc9fedfb9 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index b8099e1a1..7a402cd9a 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index 29b1a1a3a..0dbfde469 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 860faf43c..f7f7b02ca 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 0b5a3eafa..1bca741d7 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", "توجه: سرور RustDesk OSS این ویژگی را ندارد."), ("input note here", "یادداشت را اینجا وارد کنید"), ("note-at-conn-end-tip", "در پایان اتصال، یادداشت بخواهید"), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fi.rs b/src/lang/fi.rs index f76bed62c..e97263258 100644 --- a/src/lang/fi.rs +++ b/src/lang/fi.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 5b762df38..999288bc8 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", "Note : Cette fonctionnalité n’est pas disponible sous la version open-source du serveur RustDesk."), ("input note here", "saisir la note ici"), ("note-at-conn-end-tip", "Proposer d’écrire une note une fois la connexion terminée"), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ge.rs b/src/lang/ge.rs index 957cfa5a8..c104a3a34 100644 --- a/src/lang/ge.rs +++ b/src/lang/ge.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 05732c30f..39a3742c2 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 3487a1fc5..d030f482d 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 8ee281470..be1a5ee14 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", "MEGJEGYZÉS: Az OSS RustDesk kiszolgáló nem támogatja ezt a funkciót."), ("input note here", "Megjegyzés bevitele"), ("note-at-conn-end-tip", "Megjegyzés a kapcsolat végén"), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index b2ebe48be..ce2b34a6e 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index e4867016c..aad7e009b 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", "NOTA: il sistema operativo del server RustDesk non include questa funzionalità."), ("input note here", "Inserisci nota qui"), ("note-at-conn-end-tip", "Visualizza nota alla fine della connessione"), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 97933fc15..ea6ce5a1f 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 90b51b7af..f8f7b2707 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", "참고: RustDesk 서버 OSS에는 이 기능이 포함되어 있지 않습니다."), ("input note here", "여기에 노트 입력"), ("note-at-conn-end-tip", "연결이 끝날 때 메모 요청"), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 62d7345b3..e3eb5b44b 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index d9dac635b..a821391cf 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 406d5b3b9..79b26c243 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index a97ae4ee5..7c06d7699 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 50227384e..7f641bde1 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", "Opmerking: Deze functie is niet beschikbaar in de open-sourceversie van de RustDesk-server."), ("input note here", "voeg hier een opmerking toe"), ("note-at-conn-end-tip", "Vraag om een opmerking aan het einde van de verbinding"), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index b209dc7d6..1e4af5aa9 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", "UWAGA: Serwer OSS RustDesk nie obsługuje tej funkcji."), ("input note here", "Wstaw tutaj notatkę"), ("note-at-conn-end-tip", "Poproś o notatkę po zakończeniu połączenia."), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index da5595c05..29ff24b89 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index e9fb9e4ae..a4715b47f 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 3dae7ebf6..efbe758ef 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 70cf140c6..f8a5fd7c3 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", "ПРИМЕЧАНИЕ: в OSS-сервере RustDesk эта функция отсутствует."), ("input note here", "введите заметку"), ("note-at-conn-end-tip", "Запрашивать заметку в конце соединения"), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index a456fa63f..19b599d5e 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index d047dd35c..eafe3f244 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 93a9565a8..eb9102ac7 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 6aa203442..734bca256 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 8c7badab1..fb91966ec 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 7219d35ee..773f74e62 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs index 726135a94..bb6ef6f35 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 94b5386a3..3eda9e83e 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 981df49a6..932970d3f 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 74cf5767c..5db6e390d 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", "NOT: RustDesk sunucu OSS'si bu özelliği içermemektedir."), ("input note here", "Notu buraya girin"), ("note-at-conn-end-tip", "Bağlantı bittiğinde not sorulsun"), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 7a8f0ec06..55b7c89b3 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", "注意:RustDesk 開源伺服器 (OSS server) 不包含此功能。"), ("input note here", "輸入備註"), ("note-at-conn-end-tip", "在連接結束時請求備註"), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 8daf4d271..70108e8b6 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vi.rs b/src/lang/vi.rs index 58fb13656..090501015 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -729,5 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", ""), ("input note here", ""), ("note-at-conn-end-tip", ""), + ("Show terminal extra keys", ""), ].iter().cloned().collect(); } From 5af580f44d4ba523da94c1a60cd2ecb6d23fff7d Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Wed, 31 Dec 2025 06:27:16 +0100 Subject: [PATCH 048/277] Italian language update (#13913) --- src/lang/it.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index aad7e009b..b5700bf05 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -102,9 +102,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unselect All", "Deseleziona tutto"), ("Empty Directory", "Cartella vuota"), ("Not an empty directory", "Non è una cartella vuota"), - ("Are you sure you want to delete this file?", "Sei sicuro di voler eliminare questo file?"), - ("Are you sure you want to delete this empty directory?", "Sei sicuro di voler eliminare questa cartella vuota?"), - ("Are you sure you want to delete the file of this directory?", "Sei sicuro di voler eliminare il file di questa cartella?"), + ("Are you sure you want to delete this file?", "Vuoi eliminare questo file?"), + ("Are you sure you want to delete this empty directory?", "Vuoi eliminare questa cartella vuota?"), + ("Are you sure you want to delete the file of this directory?", "Vuoi eliminare il file di questa cartella?"), ("Do this for all conflicts", "Ricorda questa scelta per tutti i conflitti"), ("This is irreversible!", "Questo è irreversibile!"), ("Deleting", "Eliminazione di"), @@ -243,7 +243,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remote ID", "ID remoto"), ("Paste", "Incolla"), ("Paste here?", "Incollare qui?"), - ("Are you sure to close the connection?", "Sei sicuro di voler chiudere la connessione?"), + ("Are you sure to close the connection?", "Vuoi chiudere la connessione?"), ("Download new version", "Scarica nuova versione"), ("Touch mode", "Modalità tocco"), ("Mouse mode", "Modalità mouse"), @@ -313,7 +313,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set permanent password", "Imposta password permanente"), ("Enable remote restart", "Abilita riavvio da remoto"), ("Restart remote device", "Riavvia dispositivo remoto"), - ("Are you sure you want to restart", "Sei sicuro di voler riavviare?"), + ("Are you sure you want to restart", "Vuoi riavviare?"), ("Restarting remote device", "Il dispositivo remoto si sta riavviando"), ("remote_restarting_tip", "Riavvia il dispositivo remoto"), ("Copied", "Copiato"), @@ -502,7 +502,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Outgoing connection", "Connessioni in uscita"), ("Exit", "Esci da RustDesk"), ("Open", "Apri RustDesk"), - ("logout_tip", "Sei sicuro di voler uscire?"), + ("logout_tip", "Vuoi disconnetterti?"), ("Service", "Servizio"), ("Start", "Avvia"), ("Stop", "Ferma"), @@ -604,7 +604,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Outgoing", "In uscita"), ("Clear Wayland screen selection", "Annulla selezione schermata Wayland"), ("clear_Wayland_screen_selection_tip", "Dopo aver annullato la selezione schermo, è possibile selezionare nuovamente lo schermo da condividere."), - ("confirm_clear_Wayland_screen_selection_tip", "Sei sicuro di voler annullare la selezione schermo Wayland?"), + ("confirm_clear_Wayland_screen_selection_tip", "Vuoi annullare la selezione schermo Wayland?"), ("android_new_voice_call_tip", "È stata ricevuta una nuova richiesta di chiamata vocale. Se accetti, l'audio passerà alla comunicazione vocale."), ("texture_render_tip", "Usa il rendering texture per rendere le immagini più fluide. Se riscontri problemi di rendering prova a disabilitare questa opzione."), ("Use texture rendering", "Usa rendering texture"), @@ -623,8 +623,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Telegram bot", "Bot Telegram"), ("enable-bot-tip", "Se abiliti questa funzione, puoi ricevere il codice 2FA dal tuo bot.\nPuò anche funzionare come notifica di connessione."), ("enable-bot-desc", "1. apri una chat con @BotFather.\n2. Invia il comando \"/newbot\", dopo aver completato questo passaggio riceverai un token.\n3. Avvia una chat con il tuo bot appena creato. Per attivarlo Invia un messaggio che inizia con una barra (\"/\") tipo \"/hello\".\n"), - ("cancel-2fa-confirm-tip", "Sei sicuro di voler annullare 2FA?"), - ("cancel-bot-confirm-tip", "Sei sicuro di voler annullare Telegram?"), + ("cancel-2fa-confirm-tip", "Vuoi disabilitare 2FA?"), + ("cancel-bot-confirm-tip", "Vuoi disabilitare il bot Telegram?"), ("About RustDesk", "Info su RustDesk"), ("Send clipboard keystrokes", "Invia sequenze tasti appunti"), ("network_error_tip", "Controlla la connessione di rete, quindi seleziona 'Riprova'."), @@ -726,9 +726,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("allow-insecure-tls-fallback-tip", "Per impostazione predefinita, RustDesk verifica il certificato del server per i protocolli usando TLS.\nCon questa opzione abilitata, RustDesk salterà il passaggio di verifica e procederà in caso di errore di verifica."), ("Disable UDP", "Disabilita UDP"), ("disable-udp-tip", "Controlla se usare solo TCP.\nQuando questa opzione è abilitata, RustDesk non userà più UDP 21116, verrà invece usato TCP 21116."), - ("server-oss-not-support-tip", "NOTA: il sistema operativo del server RustDesk non include questa funzionalità."), + ("server-oss-not-support-tip", "Nota: il sistema operativo del server RustDesk non include questa funzionalità."), ("input note here", "Inserisci nota qui"), ("note-at-conn-end-tip", "Visualizza nota alla fine della connessione"), - ("Show terminal extra keys", ""), + ("Show terminal extra keys", "Visualizza tasti aggiuntivi terminale"), ].iter().cloned().collect(); } From d8932b69a3c797e02ff21495ca772b780c9f5292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?VenusGirl=E2=9D=A4?= Date: Wed, 31 Dec 2025 14:27:28 +0900 Subject: [PATCH 049/277] Update Korean (#13916) --- src/lang/ko.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/ko.rs b/src/lang/ko.rs index f8f7b2707..8ffdeefa1 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -729,6 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", "참고: RustDesk 서버 OSS에는 이 기능이 포함되어 있지 않습니다."), ("input note here", "여기에 노트 입력"), ("note-at-conn-end-tip", "연결이 끝날 때 메모 요청"), - ("Show terminal extra keys", ""), + ("Show terminal extra keys", "터미널 추가 키 표시"), ].iter().cloned().collect(); } From d27a21feeed1e9613cf60e8142eee9b00aee3696 Mon Sep 17 00:00:00 2001 From: solokot Date: Wed, 31 Dec 2025 08:27:40 +0300 Subject: [PATCH 050/277] Update ru.rs (#13917) --- src/lang/ru.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index f8a5fd7c3..ad9c84989 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -626,7 +626,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("cancel-2fa-confirm-tip", "Отключить двухфакторную аутентификацию?"), ("cancel-bot-confirm-tip", "Отключить Telegram-бота?"), ("About RustDesk", "О RustDesk"), - ("Send clipboard keystrokes", "Отправлять нажатия клавиш из буфера обмена"), + ("Send clipboard keystrokes", "Отправлять нажатия клавиш в буфер обмена"), ("network_error_tip", "Проверьте подключение к сети, затем нажмите \"Повтор\"."), ("Unlock with PIN", "Разблокировать PIN-кодом"), ("Requires at least {} characters", "Требуется не менее {} символов"), @@ -729,6 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", "ПРИМЕЧАНИЕ: в OSS-сервере RustDesk эта функция отсутствует."), ("input note here", "введите заметку"), ("note-at-conn-end-tip", "Запрашивать заметку в конце соединения"), - ("Show terminal extra keys", ""), + ("Show terminal extra keys", "Показывать дополнительные кнопки терминала"), ].iter().cloned().collect(); } From 918ce865ca9ee7786f59ffe09da6bede4fa4d688 Mon Sep 17 00:00:00 2001 From: Kratos Date: Wed, 31 Dec 2025 06:27:53 +0100 Subject: [PATCH 051/277] Update hu.rs (#13918) Translate new string --- src/lang/hu.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/hu.rs b/src/lang/hu.rs index be1a5ee14..b3777e58d 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -729,6 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", "MEGJEGYZÉS: Az OSS RustDesk kiszolgáló nem támogatja ezt a funkciót."), ("input note here", "Megjegyzés bevitele"), ("note-at-conn-end-tip", "Megjegyzés a kapcsolat végén"), - ("Show terminal extra keys", ""), + ("Show terminal extra keys", "További terminálgombok megjelenítése"), ].iter().cloned().collect(); } From 19ae785fa22b4e542f9e02cd79fa5a00670b2ac7 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Wed, 31 Dec 2025 06:28:04 +0100 Subject: [PATCH 052/277] Update de.rs (#13919) --- src/lang/de.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index f734d49b9..897eb88a1 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -729,6 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", "HINWEIS: RustDesk Server OSS enthält diese Funktion nicht."), ("input note here", "Hier eine Notiz eingeben"), ("note-at-conn-end-tip", "Am Ende der Verbindung um eine Notiz bitten."), - ("Show terminal extra keys", ""), + ("Show terminal extra keys", "Zusätzliche Tasten des Terminals anzeigen"), ].iter().cloned().collect(); } From 0758e10ae20aca827d4e076eb23e0d628f943889 Mon Sep 17 00:00:00 2001 From: Lynilia <89228568+Lynilia@users.noreply.github.com> Date: Wed, 31 Dec 2025 06:28:16 +0100 Subject: [PATCH 053/277] Update fr.rs (#13921) --- src/lang/fr.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 999288bc8..85815893e 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -728,7 +728,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("disable-udp-tip", "Contrôle l’utilisation exclusive du mode TCP.\nLorsque cette option est activée, RustDesk n’utilise plus le port UDP 21116 et utilise le port TCP 21116 à la place."), ("server-oss-not-support-tip", "Note : Cette fonctionnalité n’est pas disponible sous la version open-source du serveur RustDesk."), ("input note here", "saisir la note ici"), - ("note-at-conn-end-tip", "Proposer d’écrire une note une fois la connexion terminée"), - ("Show terminal extra keys", ""), + ("note-at-conn-end-tip", "Proposer de rédiger une note une fois la connexion terminée"), + ("Show terminal extra keys", "Afficher les touches supplémentaires du terminal"), ].iter().cloned().collect(); } From dec0e7c56d1619e50760008d95fe5aace5307fb3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 31 Dec 2025 13:28:45 +0800 Subject: [PATCH 054/277] Git submodule: Bump libs/hbb_common from `fa15710` to `12f2a47` (#13923) Bumps [libs/hbb_common](https://github.com/rustdesk/hbb_common) from `fa15710` to `12f2a47`. - [Release notes](https://github.com/rustdesk/hbb_common/releases) - [Commits](https://github.com/rustdesk/hbb_common/compare/fa157108be16b9ce58852a69c2186a3ced3c559b...12f2a47770af7521588ccaa67731806f15d0132d) --- updated-dependencies: - dependency-name: libs/hbb_common dependency-version: 12f2a47770af7521588ccaa67731806f15d0132d dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- libs/hbb_common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/hbb_common b/libs/hbb_common index fa157108b..12f2a4777 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit fa157108be16b9ce58852a69c2186a3ced3c559b +Subproject commit 12f2a47770af7521588ccaa67731806f15d0132d From 7e3f0a607ba60d671a1f069c26d1285900024c8e Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 2 Jan 2026 09:14:31 +0800 Subject: [PATCH 055/277] fix: add Content-Length header for empty body POST requests (#13940) Signed-off-by: 21pages --- flutter/lib/models/ab_model.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index 1a165ce11..b6ee7cf85 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -202,6 +202,7 @@ class AbModel { final api = "${await bind.mainGetApiServer()}/api/ab/settings"; var headers = getHttpHeaders(); headers['Content-Type'] = "application/json"; + _setEmptyBody(headers); final resp = await http.post(Uri.parse(api), headers: headers); if (resp.statusCode == 404) { debugPrint("HTTP 404, api server doesn't support shared address book"); @@ -228,6 +229,7 @@ class AbModel { final api = "${await bind.mainGetApiServer()}/api/ab/personal"; var headers = getHttpHeaders(); headers['Content-Type'] = "application/json"; + _setEmptyBody(headers); final resp = await http.post(Uri.parse(api), headers: headers); if (resp.statusCode == 404) { debugPrint("HTTP 404, current api server is legacy mode"); @@ -269,6 +271,7 @@ class AbModel { }); var headers = getHttpHeaders(); headers['Content-Type'] = "application/json"; + _setEmptyBody(headers); final resp = await http.post(uri, headers: headers); Map json = _jsonDecodeRespMap(decode_http_response(resp), resp.statusCode); @@ -1406,6 +1409,7 @@ class Ab extends BaseAb { }); var headers = getHttpHeaders(); headers['Content-Type'] = "application/json"; + _setEmptyBody(headers); final resp = await http.post(uri, headers: headers); statusCode = resp.statusCode; Map json = @@ -1463,6 +1467,7 @@ class Ab extends BaseAb { ); var headers = getHttpHeaders(); headers['Content-Type'] = "application/json"; + _setEmptyBody(headers); final resp = await http.post(uri, headers: headers); statusCode = resp.statusCode; List json = @@ -1977,3 +1982,8 @@ String _jsonDecodeActionResp(http.Response resp) { } return errMsg; } + +// https://github.com/seanmonstar/reqwest/issues/838 +void _setEmptyBody(Map headers) { + headers['Content-Length'] = '0'; +} From 9301edef0683e639607dc906a6ead118764990c9 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 2 Jan 2026 10:24:47 +0800 Subject: [PATCH 056/277] remove gzip encoding in Legacy AB pushes (#13937) Signed-off-by: 21pages --- flutter/lib/models/ab_model.dart | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index b6ee7cf85..81c4dc851 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -1015,16 +1015,8 @@ class LegacyAb extends BaseAb { var authHeaders = getHttpHeaders(); authHeaders['Content-Type'] = "application/json"; final body = jsonEncode({"data": jsonEncode(_serialize())}); - http.Response resp; - // support compression - if (licensedDevices > 0 && body.length > 1024) { - authHeaders['Content-Encoding'] = "gzip"; - resp = await http.post(Uri.parse(api), - headers: authHeaders, body: GZipCodec().encode(utf8.encode(body))); - } else { - resp = - await http.post(Uri.parse(api), headers: authHeaders, body: body); - } + http.Response resp = + await http.post(Uri.parse(api), headers: authHeaders, body: body); if (resp.statusCode == 200 && (resp.body.isEmpty || resp.body.toLowerCase() == 'null')) { ret = true; From 419703d2ea2f0439f0c555011b21857a8adf2230 Mon Sep 17 00:00:00 2001 From: Alex Rijckaert Date: Fri, 2 Jan 2026 15:11:18 +0100 Subject: [PATCH 057/277] Update dutch translation for 'Show terminal extra keys' (#13939) --- src/lang/nl.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 7f641bde1..82e049e86 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -729,6 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", "Opmerking: Deze functie is niet beschikbaar in de open-sourceversie van de RustDesk-server."), ("input note here", "voeg hier een opmerking toe"), ("note-at-conn-end-tip", "Vraag om een opmerking aan het einde van de verbinding"), - ("Show terminal extra keys", ""), + ("Show terminal extra keys", "Toon extra toetsen voor terminal"), ].iter().cloned().collect(); } From f6d6c3afb591bbbd602f523ead63dfd20d4cf282 Mon Sep 17 00:00:00 2001 From: bilimiyorum <131397022+bilimiyorum@users.noreply.github.com> Date: Fri, 2 Jan 2026 17:13:32 +0300 Subject: [PATCH 058/277] Turkish language support (#13941) New string entry --- src/lang/tr.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 5db6e390d..1ab02da5b 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -699,7 +699,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Terminal", ""), ("Enable terminal", ""), ("New tab", "Yeni sekme"), - ("Keep terminal sessions on disconnect", "Bağlantı kesildiğinde uçbirim oturumlarını açık tut"), + ("Keep terminal sessions on disconnect", "Bağlantı kesildiğinde terminal oturumlarını açık tut"), ("Terminal (Run as administrator)", "Terminal (Yönetici olarak çalıştır)"), ("terminal-admin-login-tip", "Lütfen kontrol edilen tarafın yönetici kullanıcı adı ve şifresini giriniz."), ("Failed to get user token.", "Kullanıcı belirteci alınamadı."), @@ -729,6 +729,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", "NOT: RustDesk sunucu OSS'si bu özelliği içermemektedir."), ("input note here", "Notu buraya girin"), ("note-at-conn-end-tip", "Bağlantı bittiğinde not sorulsun"), - ("Show terminal extra keys", ""), + ("Show terminal extra keys", "Terminal ek tuşlarını göster"), ].iter().cloned().collect(); } From 7ac03ffefc523bf50de7bf16033143a1b012ad7f Mon Sep 17 00:00:00 2001 From: "Re*Index. (ot_inc)" <32851879+reindex-ot@users.noreply.github.com> Date: Sat, 3 Jan 2026 13:36:25 +0900 Subject: [PATCH 059/277] Update Japanese translations in ja.rs (#13952) --- src/lang/ja.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lang/ja.rs b/src/lang/ja.rs index ea6ce5a1f..9a9b08ec2 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -721,14 +721,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", "仮想ジョイスティックを表示する"), ("Edit note", "メモを編集"), ("Alias", "エイリアス"), - ("ScrollEdge", ""), - ("Allow insecure TLS fallback", ""), - ("allow-insecure-tls-fallback-tip", ""), - ("Disable UDP", ""), - ("disable-udp-tip", ""), - ("server-oss-not-support-tip", ""), - ("input note here", ""), - ("note-at-conn-end-tip", ""), - ("Show terminal extra keys", ""), + ("ScrollEdge", "スクロールエッジ"), + ("Allow insecure TLS fallback", "安全ではない TLS フォールバックを許可する"), + ("allow-insecure-tls-fallback-tip", "既定では RustDesk は TLS を使用するプロトコルのサーバー証明書を検証します。\nこのオプションを有効化すると RustDesk は検証の手順をスキップして、検証に失敗した場合の処理を続行します。"), + ("Disable UDP", "UDP を無効化する"), + ("disable-udp-tip", "TCP のみ使用するかどうかを制御します。\nこのオプションを有効化すると、RustDesk は UDP 21116 を使用せずに TCP 21116 を使用するようになります。"), + ("server-oss-not-support-tip", "注意: RustDesk Server OSS にはこの機能が含まれていません。"), + ("input note here", "ここにメモを入力"), + ("note-at-conn-end-tip", "接続終了時にメモを要求する"), + ("Show terminal extra keys", "ターミナルの追加キーを表示する"), ].iter().cloned().collect(); } From f65952cf1cbd1bccc97aabcd78f07da2c11ae019 Mon Sep 17 00:00:00 2001 From: 21pages Date: Mon, 5 Jan 2026 22:16:35 +0800 Subject: [PATCH 060/277] fix(desktop): wakelock issue with multiple tabs in same window (#13956) Each desktop isolate now independently tracks wakelock state. WakelockPlus.disable() is only called when all tabs within the same isolate are closed/minimized. WakelockPlus ensures screen stays awake as long as any isolate has wakelock enabled. Signed-off-by: 21pages --- flutter/lib/common.dart | 26 +++++++++++++++++++ .../lib/desktop/pages/file_manager_page.dart | 10 +++---- flutter/lib/desktop/pages/remote_page.dart | 22 +++++----------- .../lib/desktop/pages/view_camera_page.dart | 22 +++++----------- 4 files changed, 41 insertions(+), 39 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 07340e16b..0804ebbf4 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -24,6 +24,7 @@ import 'package:provider/provider.dart'; import 'package:uni_links/uni_links.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:uuid/uuid.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:window_manager/window_manager.dart'; import 'package:window_size/window_size.dart' as window_size; @@ -2676,6 +2677,31 @@ class SimpleWrapper { SimpleWrapper(this.value); } +/// Wakelock manager with reference counting for desktop. +/// Ensures wakelock is only disabled when all sessions are closed/minimized. +/// +/// Note: Each isolate has its own WakelockPlus instance with independent assertion. +/// As long as one isolate has wakelock enabled, the screen stays awake. +/// This manager handles multiple tabs within the same isolate. +class WakelockManager { + static final Set _enabledKeys = {}; + + static void enable(UniqueKey key) { + if (isLinux) return; + _enabledKeys.add(key); + WakelockPlus.enable(); + } + + static void disable(UniqueKey key) { + if (isLinux) return; + if (_enabledKeys.remove(key)) { + if (_enabledKeys.isEmpty) { + WakelockPlus.disable(); + } + } + } +} + /// call this to reload current window. /// /// [Note] diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 9e554cbe8..cf97351b3 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -17,7 +17,6 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/models/file_model.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:flutter_hbb/web/dummy.dart' if (dart.library.html) 'package:flutter_hbb/web/web_unique.dart'; @@ -86,6 +85,7 @@ class _FileManagerPageState extends State final _dropMaskVisible = false.obs; // TODO impl drop mask final _overlayKeyState = OverlayKeyState(); + final _uniqueKey = UniqueKey(); late FFI _ffi; @@ -107,9 +107,7 @@ class _FileManagerPageState extends State .showLoading(translate('Connecting...'), onCancel: closeConnection); }); Get.put(_ffi, tag: 'ft_${widget.id}'); - if (!isLinux) { - WakelockPlus.enable(); - } + WakelockManager.enable(_uniqueKey); if (isWeb) { _ffi.ffiModel.updateEventListener(_ffi.sessionId, widget.id); } @@ -127,9 +125,7 @@ class _FileManagerPageState extends State model.close().whenComplete(() { _ffi.close(); _ffi.dialogManager.dismissAll(); - if (!isLinux) { - WakelockPlus.disable(); - } + WakelockManager.disable(_uniqueKey); Get.delete(tag: 'ft_${widget.id}'); }); WidgetsBinding.instance.removeObserver(this); diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index a752efe6b..3c5245bb3 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -6,7 +6,6 @@ import 'package:flutter/services.dart'; import 'package:flutter/scheduler.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:flutter_hbb/models/state_model.dart'; import '../../consts.dart'; @@ -85,6 +84,7 @@ class _RemotePageState extends State late RxBool _zoomCursor; late RxBool _remoteCursorMoved; late RxBool _keyboardEnabled; + final _uniqueKey = UniqueKey(); var _blockableOverlayState = BlockableOverlayState(); @@ -138,9 +138,7 @@ class _RemotePageState extends State _ffi.dialogManager .showLoading(translate('Connecting...'), onCancel: closeConnection); }); - if (!isLinux) { - WakelockPlus.enable(); - } + WakelockManager.enable(_uniqueKey); _ffi.ffiModel.updateEventListener(sessionId, widget.id); if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote); @@ -206,26 +204,20 @@ class _RemotePageState extends State if (isWindows) { _isWindowBlur = false; } - if (!isLinux) { - WakelockPlus.enable(); - } + WakelockManager.enable(_uniqueKey); } // When the window is unminimized, onWindowMaximize or onWindowRestore can be called when the old state was maximized or not. @override void onWindowMaximize() { super.onWindowMaximize(); - if (!isLinux) { - WakelockPlus.enable(); - } + WakelockManager.enable(_uniqueKey); } @override void onWindowMinimize() { super.onWindowMinimize(); - if (!isLinux) { - WakelockPlus.disable(); - } + WakelockManager.disable(_uniqueKey); } @override @@ -268,9 +260,7 @@ class _RemotePageState extends State await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); } - if (!isLinux) { - await WakelockPlus.disable(); - } + WakelockManager.disable(_uniqueKey); await Get.delete(tag: widget.id); removeSharedStates(widget.id); } diff --git a/flutter/lib/desktop/pages/view_camera_page.dart b/flutter/lib/desktop/pages/view_camera_page.dart index 6be074b59..c45ec4d86 100644 --- a/flutter/lib/desktop/pages/view_camera_page.dart +++ b/flutter/lib/desktop/pages/view_camera_page.dart @@ -6,7 +6,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_hbb/common/widgets/remote_input.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:flutter_hbb/models/state_model.dart'; import '../../consts.dart'; @@ -77,6 +76,7 @@ class _ViewCameraPageState extends State String keyboardMode = "legacy"; bool _isWindowBlur = false; final _cursorOverImage = false.obs; + final _uniqueKey = UniqueKey(); var _blockableOverlayState = BlockableOverlayState(); @@ -124,9 +124,7 @@ class _ViewCameraPageState extends State _ffi.dialogManager .showLoading(translate('Connecting...'), onCancel: closeConnection); }); - if (!isLinux) { - WakelockPlus.enable(); - } + WakelockManager.enable(_uniqueKey); _ffi.ffiModel.updateEventListener(sessionId, widget.id); if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote); @@ -185,26 +183,20 @@ class _ViewCameraPageState extends State if (isWindows) { _isWindowBlur = false; } - if (!isLinux) { - WakelockPlus.enable(); - } + WakelockManager.enable(_uniqueKey); } // When the window is unminimized, onWindowMaximize or onWindowRestore can be called when the old state was maximized or not. @override void onWindowMaximize() { super.onWindowMaximize(); - if (!isLinux) { - WakelockPlus.enable(); - } + WakelockManager.enable(_uniqueKey); } @override void onWindowMinimize() { super.onWindowMinimize(); - if (!isLinux) { - WakelockPlus.disable(); - } + WakelockManager.disable(_uniqueKey); } @override @@ -247,9 +239,7 @@ class _ViewCameraPageState extends State await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); } - if (!isLinux) { - await WakelockPlus.disable(); - } + WakelockManager.disable(_uniqueKey); await Get.delete(tag: widget.id); removeSharedStates(widget.id); } From 7f9506b4762a9702663f1a35c3b41920f4c2ad6a Mon Sep 17 00:00:00 2001 From: Alex Rijckaert Date: Tue, 6 Jan 2026 11:15:54 +0100 Subject: [PATCH 061/277] Update Dutch (#13970) --- src/lang/nl.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 82e049e86..cafdc74a0 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -44,7 +44,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("id_change_tip", "Alleen de letters a-z, A-Z, 0-9, - (dash), _ (underscore) kunnen worden gebruikt. De eerste letter moet a-z, A-Z zijn. De lengte moet tussen 6 en 16 liggen."), ("Website", "Website"), ("About", "Over"), - ("Slogan_tip", "Met hart gemaakt in deze chaotische wereld!"), + ("Slogan_tip", "Met hart en ziel gemaakt in deze chaotische wereld!"), ("Privacy Statement", "Privacyverklaring"), ("Mute", "Geluid uit"), ("Build Date", "Datum"), From a05b619563b5e385c5f531d3c0d3e4e26f2c2ea7 Mon Sep 17 00:00:00 2001 From: Yero~ Date: Wed, 7 Jan 2026 11:20:26 +0530 Subject: [PATCH 062/277] Fix: Window positioning out of bounds on multi-monitors setup #13828 (#13903) --- flutter/lib/common.dart | 63 ++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 0804ebbf4..8de0f2b12 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1933,44 +1933,41 @@ Future _adjustRestoreMainWindowOffset( return null; } - double? frameLeft; - double? frameTop; - double? frameRight; - double? frameBottom; - if (isDesktop || isWebDesktop) { - for (final screen in await window_size.getScreenList()) { - frameLeft = frameLeft == null - ? screen.visibleFrame.left - : min(screen.visibleFrame.left, frameLeft); - frameTop = frameTop == null - ? screen.visibleFrame.top - : min(screen.visibleFrame.top, frameTop); - frameRight = frameRight == null - ? screen.visibleFrame.right - : max(screen.visibleFrame.right, frameRight); - frameBottom = frameBottom == null - ? screen.visibleFrame.bottom - : max(screen.visibleFrame.bottom, frameBottom); + final screens = await window_size.getScreenList(); + if (screens.isNotEmpty) { + final windowRect = Rect.fromLTWH(left, top, width, height); + bool isVisible = false; + for (final screen in screens) { + final intersection = windowRect.intersect(screen.visibleFrame); + if (intersection.width >= 10.0 && intersection.height >= 10.0) { + isVisible = true; + break; + } + } + if (!isVisible) { + return null; + } + return Offset(left, top); } } - if (frameLeft == null) { - frameLeft = 0.0; - frameTop = 0.0; - frameRight = ((isDesktop || isWebDesktop) - ? kDesktopMaxDisplaySize - : kMobileMaxDisplaySize) - .toDouble(); - frameBottom = ((isDesktop || isWebDesktop) - ? kDesktopMaxDisplaySize - : kMobileMaxDisplaySize) - .toDouble(); - } + + double frameLeft = 0.0; + double frameTop = 0.0; + double frameRight = ((isDesktop || isWebDesktop) + ? kDesktopMaxDisplaySize + : kMobileMaxDisplaySize) + .toDouble(); + double frameBottom = ((isDesktop || isWebDesktop) + ? kDesktopMaxDisplaySize + : kMobileMaxDisplaySize) + .toDouble(); + final minWidth = 10.0; - if ((left + minWidth) > frameRight! || - (top + minWidth) > frameBottom! || + if ((left + minWidth) > frameRight || + (top + minWidth) > frameBottom || (left + width - minWidth) < frameLeft || - top < frameTop!) { + top < frameTop) { return null; } else { return Offset(left, top); From 9dd4fa86464a7a390716c9dada405d6ff9b81f09 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 7 Jan 2026 13:51:02 +0800 Subject: [PATCH 063/277] add options: disable-change-permanent-password, disable-change-id, disable-unlock-pin (#13929) Signed-off-by: 21pages --- .gitmodules | 3 ++- flutter/lib/common.dart | 10 ++++++++ flutter/lib/consts.dart | 4 +++ .../desktop/pages/desktop_setting_page.dart | 13 +++++++--- flutter/lib/mobile/pages/server_page.dart | 25 +++++++++++-------- libs/hbb_common | 2 +- src/core_main.rs | 12 +++++++++ src/ui/index.tis | 10 +++++--- 8 files changed, 59 insertions(+), 20 deletions(-) diff --git a/.gitmodules b/.gitmodules index d80e69aa8..5fc4a9392 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,4 @@ [submodule "libs/hbb_common"] path = libs/hbb_common - url = https://github.com/rustdesk/hbb_common + url = https://github.com/21pages/hbb_common + branch = disable-change-permanent-password diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 8de0f2b12..b4c9c6e82 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -3805,6 +3805,16 @@ setResizable(bool resizable) { isOptionFixed(String key) => bind.mainIsOptionFixed(key: key); +bool isChangePermanentPasswordDisabled() => + bind.mainGetBuildinOption(key: kOptionDisableChangePermanentPassword) == + 'Y'; + +bool isChangeIdDisabled() => + bind.mainGetBuildinOption(key: kOptionDisableChangeId) == 'Y'; + +bool isUnlockPinDisabled() => + bind.mainGetBuildinOption(key: kOptionDisableUnlockPin) == 'Y'; + bool? _isCustomClient; bool get isCustomClient { _isCustomClient ??= bind.isCustomClient(); diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 7bcadd658..aea744a78 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -180,6 +180,10 @@ const String kOptionHideSecuritySetting = "hide-security-settings"; const String kOptionHideNetworkSetting = "hide-network-settings"; const String kOptionRemovePresetPasswordWarning = "remove-preset-password-warning"; +const String kOptionDisableChangePermanentPassword = + "disable-change-permanent-password"; +const String kOptionDisableChangeId = "disable-change-id"; +const String kOptionDisableUnlockPin = "disable-unlock-pin"; const kHideUsernameOnCard = "hide-username-on-card"; const String kOptionHideHelpCards = "hide-help-cards"; diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index ab6dfe47e..a431efee4 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -825,7 +825,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { permissions(context), password(context), _Card(title: '2FA', children: [tfa()]), - _Card(title: 'ID', children: [changeId()]), + if (!isChangeIdDisabled()) + _Card(title: 'ID', children: [changeId()]), more(context), ]), ), @@ -1091,6 +1092,10 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { .indexOf(kUsePermanentPassword)] && (await bind.mainGetPermanentPassword()) .isEmpty) { + if (isChangePermanentPasswordDisabled()) { + await callback(); + return; + } setPasswordDialog(notEmptyCallback: callback); } else { await callback(); @@ -1195,7 +1200,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { enabled: tmpEnabled && !locked), if (usePassword) numericOneTimePassword, if (usePassword) radios[1], - if (usePassword) + if (usePassword && !isChangePermanentPasswordDisabled()) _SubButton('Set permanent password', setPasswordDialog, permEnabled && !locked), // if (usePassword) @@ -1218,7 +1223,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { _OptionCheckBox(context, 'allow-only-conn-window-open-tip', 'allow-only-conn-window-open', reverse: false, enabled: enabled), - if (bind.mainIsInstalled()) unlockPin() + if (bind.mainIsInstalled() && !isUnlockPinDisabled()) unlockPin() ]); } @@ -2654,7 +2659,7 @@ Widget _lock( ]).marginSymmetric(vertical: 2)), onPressed: () async { final unlockPin = bind.mainGetUnlockPin(); - if (unlockPin.isEmpty) { + if (unlockPin.isEmpty || isUnlockPinDisabled()) { bool checked = await callMainCheckSuperUserPermission(); if (checked) { onUnlock(); diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index ed4fe4d98..d2a6ed8a8 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -61,12 +61,13 @@ class _DropDownAction extends StatelessWidget { final isAllowNumericOneTimePassword = gFFI.serverModel.allowNumericOneTimePassword; return [ - PopupMenuItem( - enabled: gFFI.serverModel.connectStatus > 0, - value: "changeID", - child: Text(translate("Change ID")), - ), - const PopupMenuDivider(), + if (!isChangeIdDisabled()) + PopupMenuItem( + enabled: gFFI.serverModel.connectStatus > 0, + value: "changeID", + child: Text(translate("Change ID")), + ), + if (!isChangeIdDisabled()) const PopupMenuDivider(), PopupMenuItem( value: 'AcceptSessionsViaPassword', child: listTile( @@ -87,7 +88,8 @@ class _DropDownAction extends StatelessWidget { ), if (showPasswordOption) const PopupMenuDivider(), if (showPasswordOption && - verificationMethod != kUseTemporaryPassword) + verificationMethod != kUseTemporaryPassword && + !isChangePermanentPasswordDisabled()) PopupMenuItem( value: "setPermanentPassword", child: Text(translate("Set permanent password")), @@ -149,6 +151,10 @@ class _DropDownAction extends StatelessWidget { if (value == kUsePermanentPassword && (await bind.mainGetPermanentPassword()).isEmpty) { + if (isChangePermanentPasswordDisabled()) { + callback(); + return; + } setPasswordDialog(notEmptyCallback: callback); } else { callback(); @@ -648,9 +654,8 @@ class ConnectionManager extends StatelessWidget { return Column( children: serverModel.clients .map((client) => PaddingCard( - title: translate(client.isFileTransfer - ? "Transfer file" - : "Share screen"), + title: translate( + client.isFileTransfer ? "Transfer file" : "Share screen"), titleIcon: client.isFileTransfer ? Icon(Icons.folder_outlined) : Icon(Icons.mobile_screen_share), diff --git a/libs/hbb_common b/libs/hbb_common index 12f2a4777..73ab9575f 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 12f2a47770af7521588ccaa67731806f15d0132d +Subproject commit 73ab9575fdfc1dbf2aad477bb9a8875405661cfc diff --git a/src/core_main.rs b/src/core_main.rs index 9abfcb444..59adf3aff 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -406,6 +406,10 @@ pub fn core_main() -> Option> { println!("Settings are disabled!"); return None; } + if config::Config::is_disable_change_permanent_password() { + println!("Changing permanent password is disabled!"); + return None; + } if args.len() == 2 { if crate::platform::is_installed() && is_root() { if let Err(err) = crate::ipc::set_permanent_password(args[1].to_owned()) { @@ -419,6 +423,10 @@ pub fn core_main() -> Option> { } return None; } else if args[0] == "--set-unlock-pin" { + if config::Config::is_disable_unlock_pin() { + println!("Unlock PIN is disabled!"); + return None; + } #[cfg(feature = "flutter")] if args.len() == 2 { if crate::platform::is_installed() && is_root() { @@ -440,6 +448,10 @@ pub fn core_main() -> Option> { println!("Settings are disabled!"); return None; } + if config::Config::is_disable_change_id() { + println!("Changing ID is disabled!"); + return None; + } if args.len() == 2 { if crate::platform::is_installed() && is_root() { let old_id = crate::ipc::get_id(); diff --git a/src/ui/index.tis b/src/ui/index.tis index 966b39734..20cbb7ba2 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -16,6 +16,8 @@ const disable_ab = handler.is_disable_ab(); const hide_server_settings = handler.get_builtin_option("hide-server-settings") == "Y"; const hide_proxy_settings = handler.get_builtin_option("hide-proxy-settings") == "Y"; const hide_websocket_settings = handler.get_builtin_option("hide-websocket-settings") == "Y"; +const disable_change_permanent_password = handler.get_builtin_option("disable-change-permanent-password") == "Y"; +const disable_change_id = handler.get_builtin_option("disable-change-id") == "Y"; // html min-width, min-height not working on mac, below works for all if (incoming_only) { @@ -508,11 +510,11 @@ class MyIdMenu: Reactor.Component { {!disable_settings && is_win && handler.is_installed() ? : ""} {!disable_settings && } {!disable_settings && false && handler.using_public_server() &&
  • {svg_checkmark}{translate('Always connect via relay')}
  • } - {handler.is_ok_change_id() ?
    : ""} - {!disable_account && (username ? + {!disable_change_id && handler.is_ok_change_id() ?
    : ""} + {!disable_account && (username ?
  • {translate('Logout')} ({username})
  • :
  • {translate('Login')}
  • )} - {!disable_settings && handler.is_ok_change_id() && key_confirmed && connect_status > 0 ?
  • {translate('Change ID')}
  • : ""} + {!disable_change_id && !disable_settings && handler.is_ok_change_id() && key_confirmed && connect_status > 0 ?
  • {translate('Change ID')}
  • : ""}
  • {svg_checkmark}{translate('Dark Theme')}
  • @@ -1050,7 +1052,7 @@ class PasswordArea: Reactor.Component { { !show_password ? '' :
  • {svg_checkmark}{translate('Use permanent password')}
  • } { !show_password ? '' :
  • {svg_checkmark}{translate('Use both passwords')}
  • } { !show_password ? '' :
    } - { !show_password ? '' :
  • {translate('Set permanent password')}
  • } + { !show_password || disable_change_permanent_password ? '' :
  • {translate('Set permanent password')}
  • } { !show_password ? '' : }
  • {svg_checkmark}{translate('enable-2fa-title')}
  • From 5a183490dcc629144d0d63de04d396291fac9acd Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 7 Jan 2026 14:11:20 +0800 Subject: [PATCH 064/277] fix submodule repository (#13975) Signed-off-by: 21pages --- .gitmodules | 3 +-- libs/hbb_common | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.gitmodules b/.gitmodules index 5fc4a9392..d80e69aa8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,3 @@ [submodule "libs/hbb_common"] path = libs/hbb_common - url = https://github.com/21pages/hbb_common - branch = disable-change-permanent-password + url = https://github.com/rustdesk/hbb_common diff --git a/libs/hbb_common b/libs/hbb_common index 73ab9575f..073403edb 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 73ab9575fdfc1dbf2aad477bb9a8875405661cfc +Subproject commit 073403edbf1fffcb3acfe8cbe7582ee873b23398 From 8fe10d61eae845110a436419688c83062cd0208e Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:07:14 +0800 Subject: [PATCH 065/277] fix(terminal): linux, macOS, win as the controlled (#13930) 1. `TERM` on linux terminal. 2. `htop` command not found on macOS. 3. `vim` and `claude code cli` hung up on windows. Signed-off-by: fufesou --- Cargo.toml | 11 +- src/core_main.rs | 11 + src/platform/linux.rs | 150 ++++- src/server.rs | 2 + src/server/connection.rs | 13 +- src/server/terminal_helper.rs | 1062 ++++++++++++++++++++++++++++++++ src/server/terminal_service.rs | 430 +++++++++++-- 7 files changed, 1608 insertions(+), 71 deletions(-) create mode 100644 src/server/terminal_helper.rs diff --git a/Cargo.toml b/Cargo.toml index 0b63a8167..71894b660 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -123,10 +123,19 @@ winapi = { version = "0.3", features = [ ] } windows = { version = "0.61", features = [ "Win32", + "Win32_Foundation", + "Win32_Security", + "Win32_Security_Authorization", + "Win32_Storage_FileSystem", "Win32_System", "Win32_System_Diagnostics", - "Win32_System_Threading", "Win32_System_Diagnostics_ToolHelp", + "Win32_System_Environment", + "Win32_System_IO", + "Win32_System_Memory", + "Win32_System_Pipes", + "Win32_System_Threading", + "Win32_UI_Shell", ] } winreg = "0.11" windows-service = "0.6" diff --git a/src/core_main.rs b/src/core_main.rs index 59adf3aff..ad8154dc6 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -615,6 +615,17 @@ pub fn core_main() -> Option> { #[cfg(feature = "hwcodec")] crate::ipc::hwcodec_process(); return None; + } else if args[0] == "--terminal-helper" { + // Terminal helper process - runs as user to create ConPTY + // This is needed because ConPTY has compatibility issues with CreateProcessAsUserW + #[cfg(target_os = "windows")] + { + let helper_args: Vec = args[1..].to_vec(); + if let Err(e) = crate::server::terminal_helper::run_terminal_helper(&helper_args) { + log::error!("Terminal helper failed: {}", e); + } + } + return None; } else if args[0] == "--cm" { // call connection manager to establish connections // meanwhile, return true to call flutter window to show control panel diff --git a/src/platform/linux.rs b/src/platform/linux.rs index d5a5edac0..5e608aa08 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -35,13 +35,20 @@ static mut UNMODIFIED: bool = true; const INVALID_TERM_VALUES: [&str; 3] = ["", "unknown", "dumb"]; const SHELL_PROCESSES: [&str; 4] = ["bash", "zsh", "fish", "sh"]; +// Terminal type constants +const TERM_XTERM_256COLOR: &str = "xterm-256color"; +const TERM_SCREEN_256COLOR: &str = "screen-256color"; +const TERM_XTERM: &str = "xterm"; + lazy_static::lazy_static! { pub static ref IS_X11: bool = hbb_common::platform::linux::is_x11_or_headless(); + // Cache for TERM value - once TERM_XTERM_256COLOR is found, reuse it directly + static ref CACHED_TERM: std::sync::Mutex> = std::sync::Mutex::new(None); static ref DATABASE_XTERM_256COLOR: Option = { - match Database::from_name("xterm-256color") { + match Database::from_name(TERM_XTERM_256COLOR) { Ok(database) => Some(database), Err(err) => { - log::error!("Failed to initialize xterm-256color database: {}", err); + log::error!("Failed to initialize {} database: {}", TERM_XTERM_256COLOR, err); None } } @@ -310,12 +317,12 @@ fn start_uinput_service() { /// modern features required by many applications. fn suggest_best_term() -> String { if is_running_in_tmux() || is_running_in_screen() { - return "screen-256color".to_string(); + return TERM_SCREEN_256COLOR.to_string(); } - if term_supports_256_colors("xterm-256color") { - return "xterm-256color".to_string(); + if term_supports_256_colors(TERM_XTERM_256COLOR) { + return TERM_XTERM_256COLOR.to_string(); } - "xterm".to_string() + TERM_XTERM.to_string() } fn is_running_in_tmux() -> bool { @@ -332,7 +339,7 @@ fn supports_256_colors(db: &Database) -> bool { fn term_supports_256_colors(term: &str) -> bool { match term { - "xterm-256color" => DATABASE_XTERM_256COLOR + TERM_XTERM_256COLOR => DATABASE_XTERM_256COLOR .as_ref() .map_or(false, |db| supports_256_colors(db)), _ => Database::from_name(term).map_or(false, |db| supports_256_colors(&db)), @@ -340,25 +347,140 @@ fn term_supports_256_colors(term: &str) -> bool { } fn get_cur_term(uid: &str) -> Option { + // Check cache first - if TERM_XTERM_256COLOR was found before, reuse it + if let Ok(cache) = CACHED_TERM.lock() { + if let Some(ref cached) = *cache { + if cached == TERM_XTERM_256COLOR { + return Some(cached.clone()); + } + } + } + if uid.is_empty() { return None; } + // Check current process environment if let Ok(term) = std::env::var("TERM") { - if !INVALID_TERM_VALUES.contains(&term.as_str()) { + if term == TERM_XTERM_256COLOR { + if let Ok(mut cache) = CACHED_TERM.lock() { + *cache = Some(term.clone()); + } return Some(term); } } - for proc in SHELL_PROCESSES { - // Construct a regex pattern to match either the process name followed by '$' or 'bin/' followed by the process name. - let term = get_env("TERM", uid, &format!("{}$|bin/{}", proc, proc)); - if !INVALID_TERM_VALUES.contains(&term.as_str()) { - return Some(term); + // Collect all TERM values from shell processes, looking for TERM_XTERM_256COLOR + let terms = get_all_term_values(uid); + + // Prefer TERM_XTERM_256COLOR + if terms.iter().any(|t| t == TERM_XTERM_256COLOR) { + if let Ok(mut cache) = CACHED_TERM.lock() { + *cache = Some(TERM_XTERM_256COLOR.to_string()); + } + return Some(TERM_XTERM_256COLOR.to_string()); + } + + // Return first valid TERM if no TERM_XTERM_256COLOR found + let fallback = terms.into_iter().next(); + if let Some(ref term) = fallback { + log::debug!( + "TERM_XTERM_256COLOR not found, using fallback TERM: {}", + term + ); + } + fallback +} + +/// Get all TERM values from shell processes (bash, zsh, fish, sh). +/// Returns a Vec of unique, valid TERM values. +fn get_all_term_values(uid: &str) -> Vec { + let Ok(uid_num) = uid.parse::() else { + return Vec::new(); + }; + + // Build regex pattern to match shell processes using only argv[0] (the executable path) + // Pattern: match process name at start or after '/', followed by space or end + // e.g., "bash", "/bin/bash", "/usr/bin/zsh" + let shell_pattern = SHELL_PROCESSES + .iter() + .map(|p| format!(r"(^|/){p}(\s|$)")) + .collect::>() + .join("|"); + let Ok(re) = Regex::new(&shell_pattern) else { + return Vec::new(); + }; + + let Ok(entries) = std::fs::read_dir("/proc") else { + return Vec::new(); + }; + + let mut terms = Vec::new(); + + for entry in entries.flatten() { + let file_name = entry.file_name(); + let Some(pid_str) = file_name.to_str() else { + continue; + }; + if !pid_str.chars().all(|c| c.is_ascii_digit()) { + continue; + } + + let proc_path = entry.path(); + + // Check if process belongs to the specified uid + if let Ok(meta) = std::fs::metadata(&proc_path) { + use std::os::unix::fs::MetadataExt; + if meta.uid() != uid_num { + continue; + } + } else { + continue; + } + + // Check cmdline matches process pattern + // /proc//cmdline is a sequence of null-terminated strings; the first + // one (argv[0]) is the executable path. Match the regex only against that + // to avoid false positives from arguments (e.g., "python /path/to/bash-script.py"). + let cmdline_path = proc_path.join("cmdline"); + let Ok(cmdline) = std::fs::read(&cmdline_path) else { + continue; + }; + let exe_end = cmdline.iter().position(|&b| b == 0).unwrap_or(cmdline.len()); + let exe_str = String::from_utf8_lossy(&cmdline[..exe_end]); + if !re.is_match(&exe_str) { + continue; + } + + // Read environ and extract TERM + let environ_path = proc_path.join("environ"); + let Ok(environ) = std::fs::read(&environ_path) else { + continue; + }; + + for part in environ.split(|&b| b == 0) { + if part.is_empty() { + continue; + } + if let Some(eq) = part.iter().position(|&b| b == b'=') { + let key_bytes = &part[..eq]; + if key_bytes == b"TERM" { + let val_bytes = &part[eq + 1..]; + let term = String::from_utf8_lossy(val_bytes).into_owned(); + if !INVALID_TERM_VALUES.contains(&term.as_str()) && !terms.contains(&term) { + // Early return if we found the preferred term + if term == TERM_XTERM_256COLOR { + return vec![term]; + } + terms.push(term); + } + break; + } + } } } - None + terms } #[inline] diff --git a/src/server.rs b/src/server.rs index bdf43e36e..9d2e4b804 100644 --- a/src/server.rs +++ b/src/server.rs @@ -33,6 +33,8 @@ use video_service::VideoSource; use crate::ipc::Data; pub mod audio_service; +#[cfg(target_os = "windows")] +pub mod terminal_helper; #[cfg(not(any(target_os = "android", target_os = "ios")))] pub mod terminal_service; cfg_if::cfg_if! { diff --git a/src/server/connection.rs b/src/server/connection.rs index 3670fb7cf..ee8cad591 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -3231,12 +3231,15 @@ impl Connection { if !token.is_null() { match crate::platform::ensure_primary_token(token) { Ok(t) => { - self.terminal_user_token = Some(TerminalUserToken::CurrentLogonUser(t as _)); + self.terminal_user_token = Some(TerminalUserToken::CurrentLogonUser( + crate::terminal_service::UserToken::new(t as usize), + )); } Err(e) => { log::error!("Failed to ensure primary token: {}", e); - self.terminal_user_token = - Some(TerminalUserToken::CurrentLogonUser(token as _)); + self.terminal_user_token = Some(TerminalUserToken::CurrentLogonUser( + crate::terminal_service::UserToken::new(token as usize), + )); } } None @@ -5049,9 +5052,9 @@ impl Drop for Connection { #[cfg(target_os = "windows")] if let Some(TerminalUserToken::CurrentLogonUser(token)) = self.terminal_user_token.take() { - if token != 0 { + if token.as_raw() != 0 { unsafe { - hbb_common::allow_err!(CloseHandle(HANDLE(token as _))); + hbb_common::allow_err!(CloseHandle(HANDLE(token.as_raw() as _))); }; } } diff --git a/src/server/terminal_helper.rs b/src/server/terminal_helper.rs new file mode 100644 index 000000000..8edf4621b --- /dev/null +++ b/src/server/terminal_helper.rs @@ -0,0 +1,1062 @@ +//! Terminal Helper Process +//! +//! This module implements a helper process that runs as the logged-in user and creates +//! the ConPTY + Shell. This is necessary because ConPTY has compatibility issues with +//! CreateProcessAsUserW when the ConPTY is created by a different user (SYSTEM service). +//! +//! Architecture: +//! ``` +//! SYSTEM Service (terminal_service.rs) +//! | +//! +-- CreateProcessAsUserW --> Terminal Helper (this module, runs as user) +//! | | +//! | +-- CreateProcessW + ConPTY --> Shell +//! | | +//! +-- Named Pipes <----------------+ +//! ``` +//! +//! This module also contains Windows-specific utility functions used by terminal_service.rs: +//! - Named pipe creation and connection +//! - User token and SID handling +//! - Helper process launching + +use hbb_common::{ + anyhow::{anyhow, Context, Result}, + log, +}; +use portable_pty::{CommandBuilder, MasterPty, PtySize}; +use std::{ + ffi::{c_void, OsStr}, + fs::File, + io::{Read, Write}, + os::windows::{ffi::OsStrExt, io::FromRawHandle, raw::HANDLE as RawHandle}, + ptr, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Mutex, + }, + thread, + time::Duration, +}; + +use windows::{ + core::{PCWSTR, PWSTR}, + Win32::{ + Foundation::{ + CloseHandle, LocalFree, ERROR_IO_PENDING, ERROR_PIPE_CONNECTED, HANDLE, HLOCAL, + INVALID_HANDLE_VALUE, WAIT_OBJECT_0, + }, + Security::{ + Authorization::{ + SetEntriesInAclW, EXPLICIT_ACCESS_W, SET_ACCESS, TRUSTEE_IS_SID, TRUSTEE_IS_USER, + TRUSTEE_W, + }, + CreateWellKnownSid, GetLengthSid, GetTokenInformation, InitializeSecurityDescriptor, + SetSecurityDescriptorDacl, TokenUser, WinLocalSystemSid, ACE_FLAGS, ACL, + PSECURITY_DESCRIPTOR, PSID, SECURITY_ATTRIBUTES, TOKEN_USER, + }, + Storage::FileSystem::{ + CreateFileW, FILE_ALL_ACCESS, FILE_FLAGS_AND_ATTRIBUTES, FILE_FLAG_OVERLAPPED, + FILE_GENERIC_READ, FILE_GENERIC_WRITE, FILE_SHARE_READ, FILE_SHARE_WRITE, + OPEN_EXISTING, + }, + System::{ + Environment::{CreateEnvironmentBlock, DestroyEnvironmentBlock}, + Pipes::{ + ConnectNamedPipe, CreateNamedPipeW, PIPE_READMODE_BYTE, PIPE_TYPE_BYTE, PIPE_WAIT, + }, + Threading::{ + CreateEventW, CreateProcessAsUserW, WaitForSingleObject, CREATE_NO_WINDOW, + CREATE_UNICODE_ENVIRONMENT, PROCESS_CREATION_FLAGS, PROCESS_INFORMATION, + STARTUPINFOW, + }, + IO::{GetOverlappedResult, OVERLAPPED}, + }, + }, +}; + +// Re-export types needed by terminal_service.rs +pub use windows::Win32::{ + Foundation::{ + CloseHandle as WinCloseHandle, HANDLE as WinHANDLE, WAIT_OBJECT_0 as WIN_WAIT_OBJECT_0, + }, + System::Threading::{ + GetExitCodeProcess as WinGetExitCodeProcess, TerminateProcess as WinTerminateProcess, + WaitForSingleObject as WinWaitForSingleObject, + }, +}; + +/// User token wrapper for cross-module use. +/// +/// Using newtype pattern for type safety. The inner value is `usize` to match +/// platform pointer size (32-bit on x86, 64-bit on x64). +/// Windows HANDLE is defined as `*mut c_void`, which has the same size as `usize`. +/// +/// # Design Note +/// This type is defined here (terminal_helper.rs) for Windows and in +/// terminal_service.rs for non-Windows platforms. This avoids circular +/// dependencies while keeping the API consistent across platforms. +/// Both definitions MUST have identical public API (new, as_raw methods). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct UserToken(pub usize); + +impl UserToken { + /// Create a new UserToken from a raw handle value. + pub fn new(handle: usize) -> Self { + Self(handle) + } + + /// Get the raw handle value. + pub fn as_raw(&self) -> usize { + self.0 + } +} + +// Windows pipe access mode constants (not exported by windows crate) +const PIPE_ACCESS_INBOUND: u32 = 0x00000001; +const PIPE_ACCESS_OUTBOUND: u32 = 0x00000002; + +// Named pipe configuration constants +const PIPE_BUFFER_SIZE: u32 = 65536; // 64KB for better throughput with large terminal output +const PIPE_DEFAULT_TIMEOUT_MS: u32 = 5000; +/// Timeout for waiting for helper process to connect to pipes +pub const PIPE_CONNECTION_TIMEOUT_MS: u32 = 10000; + +/// Message type constants for helper protocol. +/// Used to distinguish between terminal data and control commands. +/// Note: Using non-zero values to make debugging easier (0x00 could indicate uninitialized memory). +pub const MSG_TYPE_DATA: u8 = 0x01; +pub const MSG_TYPE_RESIZE: u8 = 0x02; + +/// Message header size: 1 byte type + 4 bytes length +pub const MSG_HEADER_SIZE: usize = 5; + +/// Maximum payload size to prevent denial of service from malicious messages. +/// 16MB should be more than enough for any legitimate terminal data. +const MAX_PAYLOAD_SIZE: usize = 16 * 1024 * 1024; + +/// Timeout in milliseconds to wait for helper process to exit gracefully before force termination. +/// Using 500ms to allow helper process enough time to clean up, especially under high system load. +pub const HELPER_GRACEFUL_EXIT_TIMEOUT_MS: u64 = 500; + +/// Information about a launched helper process. +/// Contains both the process handle and PID for tracking and status checks. +#[derive(Debug)] +pub struct HelperProcessInfo { + /// Process handle for termination and waiting + pub handle: HANDLE, + /// Process ID for logging and status display + pub pid: u32, +} + +/// Wrapper for Windows HANDLE that implements Send. +/// This is safe because Windows HANDLEs are valid across threads. +/// Note: We only implement Send, not Sync. The handle is protected by +/// Mutex in TerminalSession, so concurrent access is controlled there. +/// +/// # Ownership and Cleanup +/// This type intentionally does NOT implement Drop. The handle is owned by +/// `TerminalSession` and explicitly closed in `TerminalSession::close_internal()` +/// after graceful shutdown logic (waiting for helper to exit, force termination if needed). +/// Implementing Drop here would interfere with that cleanup sequence. +#[derive(Debug)] +pub struct SendableHandle(HANDLE); + +impl SendableHandle { + /// Create a new SendableHandle from a raw HANDLE. + pub fn new(handle: HANDLE) -> Self { + Self(handle) + } + + /// Get the raw HANDLE value. + pub fn as_raw(&self) -> HANDLE { + self.0 + } +} + +unsafe impl Send for SendableHandle {} + +/// RAII wrapper for Windows HANDLE that automatically closes the handle on drop. +/// This ensures proper resource cleanup even when errors occur or code paths diverge. +pub struct OwnedHandle(HANDLE); + +impl OwnedHandle { + /// Create a new OwnedHandle from a raw HANDLE. + /// The handle will be closed when this OwnedHandle is dropped. + pub fn new(handle: HANDLE) -> Self { + Self(handle) + } + + /// Consume the OwnedHandle and return the raw HANDLE without closing it. + /// Use this when transferring ownership to another resource (e.g., File). + pub fn into_raw(self) -> HANDLE { + let handle = self.0; + std::mem::forget(self); // Prevent Drop from closing the handle + handle + } + + /// Get the raw HANDLE value. + pub fn as_raw(&self) -> HANDLE { + self.0 + } +} + +impl Drop for OwnedHandle { + fn drop(&mut self) { + if self.0 != INVALID_HANDLE_VALUE && !self.0.is_invalid() { + unsafe { + let _ = CloseHandle(self.0); + } + } + } +} + +/// RAII guard for helper process that terminates the process on drop. +/// This prevents helper process leaks when pipe connection fails or other errors occur. +/// +/// Unlike OwnedHandle (which only closes the handle), this guard: +/// 1. Terminates the process using TerminateProcess +/// 2. Then closes the handle +/// +/// Use `disarm()` to prevent termination when the helper is successfully handed off +/// to the terminal session for proper lifecycle management. +pub struct HelperProcessGuard { + handle: HANDLE, + pid: u32, + armed: bool, +} + +impl HelperProcessGuard { + /// Create a new guard for a helper process. + pub fn new(handle: HANDLE, pid: u32) -> Self { + Self { + handle, + pid, + armed: true, + } + } + + /// Get the raw process HANDLE. + pub fn as_raw(&self) -> HANDLE { + self.handle + } + + /// Get the process ID. + pub fn pid(&self) -> u32 { + self.pid + } + + /// Disarm the guard and return the raw HANDLE. + /// After calling this, the guard will NOT terminate the process on drop. + /// Use this when successfully handing off the helper to session management. + pub fn disarm(self) -> HANDLE { + let handle = self.handle; + std::mem::forget(self); // Prevent Drop from running + handle + } +} + +impl Drop for HelperProcessGuard { + fn drop(&mut self) { + if self.armed && self.handle != INVALID_HANDLE_VALUE && !self.handle.is_invalid() { + log::warn!( + "HelperProcessGuard: terminating leaked helper process (PID {})", + self.pid + ); + unsafe { + // Terminate the process first + let _ = WinTerminateProcess(self.handle, 1); + // Then close the handle + let _ = CloseHandle(self.handle); + } + } + } +} + +/// Encode a message for the helper protocol. +/// Format: [type: u8][length: u32 LE][payload: bytes] +pub fn encode_helper_message(msg_type: u8, payload: &[u8]) -> Vec { + let mut msg = Vec::with_capacity(MSG_HEADER_SIZE + payload.len()); + msg.push(msg_type); + msg.extend_from_slice(&(payload.len() as u32).to_le_bytes()); + msg.extend_from_slice(payload); + msg +} + +/// Encode a resize message for the helper protocol. +/// Payload: rows (u16 LE) + cols (u16 LE) +pub fn encode_resize_message(rows: u16, cols: u16) -> Vec { + let mut payload = Vec::with_capacity(4); + payload.extend_from_slice(&rows.to_le_bytes()); + payload.extend_from_slice(&cols.to_le_bytes()); + encode_helper_message(MSG_TYPE_RESIZE, &payload) +} + +/// Get the default shell for Windows. +pub fn get_default_shell() -> String { + // Try PowerShell Core first (absolute paths only) + let pwsh_paths = [ + "pwsh.exe", + r"C:\Program Files\PowerShell\7\pwsh.exe", + r"C:\Program Files\PowerShell\6\pwsh.exe", + ]; + + for path in &pwsh_paths { + if std::path::Path::new(path).exists() { + log::debug!("Found PowerShell Core: {}", path); + return path.to_string(); + } + } + + // Try Windows PowerShell + let powershell_path = r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"; + if std::path::Path::new(powershell_path).exists() { + return powershell_path.to_string(); + } + + // Fallback to cmd.exe + std::env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".to_string()) +} + +/// Get the SID of the user from a token. +/// Returns a Vec containing the SID bytes. +pub fn get_user_sid_from_token(user_token: UserToken) -> Result> { + let token_handle = HANDLE(user_token.as_raw() as _); + + // First call to get required buffer size + let mut return_length = 0u32; + let _ = unsafe { GetTokenInformation(token_handle, TokenUser, None, 0, &mut return_length) }; + + if return_length == 0 { + return Err(anyhow!( + "Failed to get token information size: {}", + std::io::Error::last_os_error() + )); + } + + // Allocate buffer and get token information + let mut buffer = vec![0u8; return_length as usize]; + unsafe { + GetTokenInformation( + token_handle, + TokenUser, + Some(buffer.as_mut_ptr() as *mut c_void), + return_length, + &mut return_length, + ) + .map_err(|e| anyhow!("Failed to get token information: {}", e))?; + } + + // Extract SID from TOKEN_USER structure + let token_user = unsafe { &*(buffer.as_ptr() as *const TOKEN_USER) }; + let sid_ptr = token_user.User.Sid; + + // Get SID length and copy to owned buffer + let sid_length = unsafe { GetLengthSid(sid_ptr) }; + + if sid_length == 0 { + return Err(anyhow!("Invalid SID length")); + } + + let mut sid_buffer = vec![0u8; sid_length as usize]; + unsafe { + ptr::copy_nonoverlapping( + sid_ptr.0 as *const u8, + sid_buffer.as_mut_ptr(), + sid_length as usize, + ); + } + + Ok(sid_buffer) +} + +/// Create a restricted DACL that only allows SYSTEM and a specific user. +/// Returns a pointer to the ACL that must be freed with LocalFree. +/// +/// # Safety +/// +/// This function is safe to call, but contains internal unsafe code that relies on +/// pointer lifetime guarantees: +/// +/// - The `user_sid` slice must contain valid SID binary data. +/// - Internally, raw pointers to `system_sid_buffer` (stack-allocated) and `user_sid` +/// are stored in `TRUSTEE_W.ptstrName` fields. These pointers are only used during +/// the `SetEntriesInAclW` call, which occurs before either buffer goes out of scope. +/// - The returned ACL pointer is allocated by Windows and must be freed with `LocalFree`. +pub fn create_restricted_dacl(user_sid: &[u8]) -> Result<*mut c_void> { + // Create SYSTEM SID (well-known SID: S-1-5-18) + // SAFETY: This buffer must outlive the TRUSTEE_W structures that reference it + let mut system_sid_buffer = vec![0u8; 64]; // Max SID size + let mut system_sid_size = system_sid_buffer.len() as u32; + unsafe { + CreateWellKnownSid( + WinLocalSystemSid, + None, // No domain SID + Some(PSID(system_sid_buffer.as_mut_ptr() as *mut c_void)), + &mut system_sid_size, + ) + .map_err(|e| anyhow!("Failed to create SYSTEM SID: {}", e))?; + } + + // Build EXPLICIT_ACCESS entries for SYSTEM and user + // SAFETY: The ptstrName pointers below reference system_sid_buffer and user_sid. + // These buffers must remain valid until SetEntriesInAclW returns. + let mut explicit_access: [EXPLICIT_ACCESS_W; 2] = unsafe { std::mem::zeroed() }; + + // Entry 0: SYSTEM - full access + explicit_access[0].grfAccessPermissions = FILE_ALL_ACCESS.0; + explicit_access[0].grfAccessMode = SET_ACCESS; + explicit_access[0].grfInheritance = ACE_FLAGS(0); // No inheritance for pipes + explicit_access[0].Trustee = TRUSTEE_W { + pMultipleTrustee: ptr::null_mut(), + MultipleTrusteeOperation: Default::default(), + TrusteeForm: TRUSTEE_IS_SID, + TrusteeType: TRUSTEE_IS_USER, + ptstrName: PWSTR::from_raw(system_sid_buffer.as_ptr() as *mut u16), + }; + + // Entry 1: User - full access + explicit_access[1].grfAccessPermissions = FILE_ALL_ACCESS.0; + explicit_access[1].grfAccessMode = SET_ACCESS; + explicit_access[1].grfInheritance = ACE_FLAGS(0); // No inheritance for pipes + // SAFETY: When TrusteeForm is TRUSTEE_IS_SID, ptstrName is interpreted as a PSID + // pointer, not a string pointer. The Windows API reuses this field for different + // purposes based on TrusteeForm. The SID binary data in user_sid is valid for + // the duration of this function call (until SetEntriesInAclW returns). + explicit_access[1].Trustee = TRUSTEE_W { + pMultipleTrustee: ptr::null_mut(), + MultipleTrusteeOperation: Default::default(), + TrusteeForm: TRUSTEE_IS_SID, + TrusteeType: TRUSTEE_IS_USER, + ptstrName: PWSTR::from_raw(user_sid.as_ptr() as *mut u16), + }; + + // Create ACL from explicit access entries + // After this call returns, system_sid_buffer and user_sid are no longer needed + let mut new_acl: *mut ACL = ptr::null_mut(); + let result = unsafe { + SetEntriesInAclW( + Some(&explicit_access), + None, // No existing ACL + &mut new_acl, + ) + }; + + if result.0 != 0 { + return Err(anyhow!( + "SetEntriesInAclW failed with error code: {}", + result.0 + )); + } + + if new_acl.is_null() { + return Err(anyhow!("SetEntriesInAclW returned null ACL")); + } + + Ok(new_acl as *mut c_void) +} + +/// Create a named pipe with a restricted DACL. +/// Only SYSTEM and the specified user can access the pipe. +/// +/// # Arguments +/// * `pipe_name` - The name of the pipe to create +/// * `for_input` - True if service writes to this pipe (helper reads), false otherwise +/// * `user_token` - Required user token for creating restricted DACL +/// +/// # Security +/// +/// The restricted DACL limits pipe access to: +/// - SYSTEM account (the service) +/// - The specific user whose token was provided (the helper process) +/// +/// This function requires a valid user_token and will fail if DACL creation fails, +/// rather than falling back to a less secure NULL DACL. +pub fn create_named_pipe_server( + pipe_name: &str, + for_input: bool, + user_token: UserToken, +) -> Result { + // SECURITY_DESCRIPTOR minimum length is 40 bytes on x64. + const SD_BUFFER_SIZE: usize = 64; + const _: () = assert!( + SD_BUFFER_SIZE >= 40, + "SD_BUFFER_SIZE must be at least 40 bytes for SECURITY_DESCRIPTOR" + ); + + let mut sd_buffer = [0u8; SD_BUFFER_SIZE]; + let sd_ptr = PSECURITY_DESCRIPTOR(sd_buffer.as_mut_ptr() as *mut c_void); + + // Initialize security descriptor + unsafe { + InitializeSecurityDescriptor(sd_ptr, 1) + .map_err(|e| anyhow!("Failed to initialize security descriptor: {}", e))?; + } + + // Create restricted DACL - fail if this doesn't work (no NULL DACL fallback) + let user_sid = get_user_sid_from_token(user_token) + .context("Failed to get user SID from token for pipe DACL")?; + let acl_ptr = + create_restricted_dacl(&user_sid).context("Failed to create restricted DACL for pipe")?; + + log::debug!("Created restricted DACL for pipe: {}", pipe_name); + + // Set DACL on security descriptor + unsafe { + SetSecurityDescriptorDacl(sd_ptr, true, Some(acl_ptr as *const _ as *const _), false) + .map_err(|e| { + // Clean up ACL on error (ignore result - cleanup is best-effort, original error takes precedence) + let _ = LocalFree(Some(HLOCAL(acl_ptr))); + anyhow!("Failed to set restricted DACL: {}", e) + })?; + } + + let sa = SECURITY_ATTRIBUTES { + nLength: std::mem::size_of::() as u32, + lpSecurityDescriptor: sd_buffer.as_mut_ptr() as *mut c_void, + bInheritHandle: false.into(), + }; + + let wide_name: Vec = OsStr::new(pipe_name) + .encode_wide() + .chain(std::iter::once(0)) + .collect(); + + let access_mode = if for_input { + FILE_FLAGS_AND_ATTRIBUTES(PIPE_ACCESS_INBOUND | FILE_FLAG_OVERLAPPED.0) + } else { + FILE_FLAGS_AND_ATTRIBUTES(PIPE_ACCESS_OUTBOUND | FILE_FLAG_OVERLAPPED.0) + }; + + log::debug!( + "Creating named pipe: {} (for_input={}, restricted_dacl=true)", + pipe_name, + for_input + ); + + let handle = unsafe { + CreateNamedPipeW( + PCWSTR::from_raw(wide_name.as_ptr()), + access_mode, + PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, + 1, // max instances + PIPE_BUFFER_SIZE, + PIPE_BUFFER_SIZE, + PIPE_DEFAULT_TIMEOUT_MS, + Some(&sa), + ) + }; + + // Clean up ACL after pipe creation (security descriptor has been applied) + // Ignore result: LocalFree failure is non-critical since the pipe is already created + unsafe { + let _ = LocalFree(Some(HLOCAL(acl_ptr))); + } + + if handle == INVALID_HANDLE_VALUE { + return Err(anyhow!( + "Failed to create named pipe {}: {}", + pipe_name, + std::io::Error::last_os_error() + )); + } + + log::debug!("Named pipe created: {}", pipe_name); + Ok(handle) +} + +/// Wait for client to connect to named pipe with timeout. +/// +/// # Ownership +/// This function **takes ownership** of the `pipe_handle` via OwnedHandle: +/// - On success: the handle is extracted and wrapped in a `File`. +/// - On failure: the handle is automatically closed when OwnedHandle drops. +pub fn wait_for_pipe_connection( + pipe_handle: OwnedHandle, + pipe_name: &str, + timeout_ms: u32, +) -> Result { + log::debug!("Waiting for pipe connection: {}", pipe_name); + + // Create an event for overlapped I/O (also wrapped in OwnedHandle for RAII) + let event = unsafe { CreateEventW(None, true, false, PCWSTR::null()) } + .map_err(|e| anyhow!("Failed to create event for pipe connection: {}", e))?; + let event_handle = OwnedHandle::new(event); + + let mut overlapped: OVERLAPPED = unsafe { std::mem::zeroed() }; + overlapped.hEvent = event_handle.as_raw(); + + let result = unsafe { ConnectNamedPipe(pipe_handle.as_raw(), Some(&mut overlapped)) }; + if result.is_err() { + let err = std::io::Error::last_os_error(); + let err_code = err.raw_os_error().unwrap_or(0); + + // ERROR_PIPE_CONNECTED means client already connected, which is OK + if err_code == ERROR_PIPE_CONNECTED.0 as i32 { + log::debug!("Pipe already connected: {}", pipe_name); + return Ok(unsafe { File::from_raw_handle(pipe_handle.into_raw().0 as RawHandle) }); + } + + // ERROR_IO_PENDING means we need to wait + if err_code == ERROR_IO_PENDING.0 as i32 { + log::debug!("Pipe connection pending, waiting with timeout..."); + let wait_result = unsafe { WaitForSingleObject(event_handle.as_raw(), timeout_ms) }; + + if wait_result != WAIT_OBJECT_0 { + log::error!("Timeout waiting for pipe connection: {}", pipe_name); + return Err(anyhow!( + "Timeout waiting for pipe connection: {}", + pipe_name + )); + } + + // Check if connection was successful + let mut bytes_transferred = 0u32; + let overlapped_result = unsafe { + GetOverlappedResult( + pipe_handle.as_raw(), + &overlapped, + &mut bytes_transferred, + false, + ) + }; + if overlapped_result.is_err() { + let err = std::io::Error::last_os_error(); + log::error!("Failed to complete pipe connection {}: {}", pipe_name, err); + return Err(anyhow!( + "Failed to complete pipe connection {}: {}", + pipe_name, + err + )); + } + + log::debug!("Pipe connected: {}", pipe_name); + } else { + log::error!("Failed to connect named pipe {}: {}", pipe_name, err); + return Err(anyhow!( + "Failed to connect named pipe {}: {}", + pipe_name, + err + )); + } + } else { + log::debug!("Pipe connected immediately: {}", pipe_name); + } + + // Success: transfer pipe ownership to File, event_handle drops + Ok(unsafe { File::from_raw_handle(pipe_handle.into_raw().0 as RawHandle) }) +} + +/// Launch terminal helper process as the logged-in user using the provided token. +/// The helper process creates ConPTY and shell, communicating via named pipes. +/// This uses CreateProcessAsUserW directly with the user token, which works because +/// the helper process itself doesn't need ConPTY - it creates ConPTY internally. +/// +/// Returns HelperProcessInfo containing the process handle and PID. + +/// RAII guard for environment block cleanup. +/// Ensures DestroyEnvironmentBlock is called even if an error occurs. +struct EnvironmentBlockGuard { + ptr: *mut c_void, +} + +impl Drop for EnvironmentBlockGuard { + fn drop(&mut self) { + if !self.ptr.is_null() { + unsafe { + // Ignore result: DestroyEnvironmentBlock failure is non-critical during cleanup + let _ = DestroyEnvironmentBlock(self.ptr); + } + } + } +} + +pub fn launch_terminal_helper_with_token( + user_token: UserToken, + input_pipe_name: &str, + output_pipe_name: &str, + terminal_id: i32, + rows: u16, + cols: u16, +) -> Result { + let exe_path = + std::env::current_exe().map_err(|e| anyhow!("Failed to get current exe path: {}", e))?; + + // Build command line arguments (without exe path to avoid escaping issues) + // lpApplicationName will contain the exe path separately + let cmd_args = format!( + "--terminal-helper {} {} {} {} {}", + input_pipe_name, output_pipe_name, rows, cols, terminal_id + ); + + log::debug!("Launching terminal helper for terminal {}", terminal_id); + + // Convert exe path to wide string for lpApplicationName + let exe_path_wide: Vec = OsStr::new(exe_path.as_os_str()) + .encode_wide() + .chain(std::iter::once(0)) + .collect(); + + // Command line must include exe name as first argument per Windows convention + let cmd_line = format!("\"{}\" {}", exe_path.display(), cmd_args); + let mut cmd_wide: Vec = OsStr::new(&cmd_line) + .encode_wide() + .chain(std::iter::once(0)) + .collect(); + + let mut si: STARTUPINFOW = unsafe { std::mem::zeroed() }; + si.cb = std::mem::size_of::() as u32; + + let mut pi: PROCESS_INFORMATION = unsafe { std::mem::zeroed() }; + + // Create environment block for the user with RAII cleanup + let mut environment: *mut c_void = ptr::null_mut(); + let env_ok = unsafe { + CreateEnvironmentBlock( + &mut environment, + Some(HANDLE(user_token.as_raw() as _)), + true, + ) + } + .is_ok(); + + // Use RAII guard to ensure cleanup even on error paths + let _env_guard = if env_ok && !environment.is_null() { + Some(EnvironmentBlockGuard { ptr: environment }) + } else { + if !env_ok { + log::warn!("Failed to create environment block, using default"); + } + None + }; + + let creation_flags = CREATE_NO_WINDOW + | if env_ok { + CREATE_UNICODE_ENVIRONMENT + } else { + PROCESS_CREATION_FLAGS(0) + }; + + // Use lpApplicationName to pass exe path separately from command line + // This avoids potential issues with special characters in the exe path + let result = unsafe { + CreateProcessAsUserW( + Some(HANDLE(user_token.as_raw() as _)), + PCWSTR::from_raw(exe_path_wide.as_ptr()), // lpApplicationName: exe path + Some(PWSTR::from_raw(cmd_wide.as_mut_ptr())), // lpCommandLine: full command + None, + None, + false, // Don't inherit handles + creation_flags, + if env_ok { Some(environment) } else { None }, + PCWSTR::null(), // Use default current directory + &si, + &mut pi, + ) + }; + + // Environment block cleanup is handled by _env_guard's Drop + + if let Err(e) = result { + log::error!("CreateProcessAsUserW failed: {}", e); + return Err(anyhow!("Failed to launch terminal helper: {}", e)); + } + + // Close thread handle - we only need the process handle for tracking + // Ignore result: CloseHandle failure here is non-critical since process is already launched + unsafe { + let _ = CloseHandle(pi.hThread); + } + + log::info!("Terminal helper launched with PID {}", pi.dwProcessId); + // Return process info for tracking + Ok(HelperProcessInfo { + handle: pi.hProcess, + pid: pi.dwProcessId, + }) +} + +/// Check if a helper process is still running. +/// Returns true if the process is running, false if it has exited. +pub fn is_helper_process_running(handle: HANDLE) -> bool { + let wait_result = unsafe { WaitForSingleObject(handle, 0) }; + // WAIT_TIMEOUT (258) means process is still running + // WAIT_OBJECT_0 (0) means process has exited + wait_result != WAIT_OBJECT_0 +} + +/// Run terminal helper process +/// Args: --terminal-helper +pub fn run_terminal_helper(args: &[String]) -> Result<()> { + if args.len() < 5 { + return Err(anyhow!( + "Usage: --terminal-helper " + )); + } + + let input_pipe_name = &args[0]; + let output_pipe_name = &args[1]; + let rows: u16 = args[2] + .parse() + .map_err(|e| anyhow!("Failed to parse rows '{}': {}", args[2], e))?; + let cols: u16 = args[3] + .parse() + .map_err(|e| anyhow!("Failed to parse cols '{}': {}", args[3], e))?; + let terminal_id: i32 = args[4] + .parse() + .map_err(|e| anyhow!("Failed to parse terminal_id '{}': {}", args[4], e))?; + + log::debug!( + "Terminal helper starting: terminal_id={}, size={}x{}", + terminal_id, + cols, + rows + ); + + // Open named pipes (created by the service) + let mut input_pipe = open_pipe(input_pipe_name, true)?; + let mut output_pipe = open_pipe(output_pipe_name, false)?; + + // Create ConPTY and shell + let pty_size = PtySize { + rows, + cols, + pixel_width: 0, + pixel_height: 0, + }; + + let pty_system = portable_pty::native_pty_system(); + let pty_pair = pty_system.openpty(pty_size).context("Failed to open PTY")?; + + let shell = get_default_shell(); + log::debug!("Using shell: {}", shell); + + let cmd = CommandBuilder::new(&shell); + let mut child = pty_pair + .slave + .spawn_command(cmd) + .context("Failed to spawn shell")?; + + // Explicitly drop slave after spawning to release resources + drop(pty_pair.slave); + + let pid = child.process_id().unwrap_or(0); + log::debug!("Shell started with PID: {}", pid); + + let mut pty_writer = pty_pair + .master + .take_writer() + .context("Failed to get PTY writer")?; + + let mut pty_reader = pty_pair + .master + .try_clone_reader() + .context("Failed to get PTY reader")?; + + // Wrap pty_pair.master in Arc for sharing with input thread (for resize). + let pty_master: Arc>> = Arc::new(Mutex::new(pty_pair.master)); + + let exiting = Arc::new(AtomicBool::new(false)); + + // Thread: Read from input pipe, parse messages, write data to PTY or handle control commands + let exiting_clone = exiting.clone(); + let pty_master_clone = pty_master.clone(); + let input_thread = thread::spawn(move || { + let mut input_pipe = input_pipe; + let mut header_buf = [0u8; MSG_HEADER_SIZE]; + let mut payload_buf = vec![0u8; 4096]; + + loop { + if exiting_clone.load(Ordering::SeqCst) { + break; + } + + // Read message header + match read_exact_or_eof(&mut input_pipe, &mut header_buf) { + Ok(false) => { + log::debug!("Input pipe EOF"); + break; + } + Ok(true) => {} + Err(e) => { + log::error!("Input pipe header read error: {}", e); + break; + } + } + + let msg_type = header_buf[0]; + let payload_len = + u32::from_le_bytes([header_buf[1], header_buf[2], header_buf[3], header_buf[4]]) + as usize; + + // Validate payload length to prevent denial of service + if payload_len > MAX_PAYLOAD_SIZE { + log::error!( + "Payload too large: {} bytes (max {})", + payload_len, + MAX_PAYLOAD_SIZE + ); + break; + } + + // Ensure payload buffer is large enough + if payload_buf.len() < payload_len { + payload_buf.resize(payload_len, 0); + } + + // Read payload + if payload_len > 0 { + match read_exact_or_eof(&mut input_pipe, &mut payload_buf[..payload_len]) { + Ok(false) => { + log::debug!("Input pipe EOF during payload read"); + break; + } + Ok(true) => {} + Err(e) => { + log::error!("Input pipe payload read error: {}", e); + break; + } + } + } + + match msg_type { + MSG_TYPE_DATA => { + // Write terminal data to PTY + if let Err(e) = pty_writer.write_all(&payload_buf[..payload_len]) { + log::error!("PTY write error: {}", e); + break; + } + if let Err(e) = pty_writer.flush() { + log::error!("PTY flush error: {}", e); + break; + } + } + MSG_TYPE_RESIZE => { + if payload_len >= 4 { + let rows = u16::from_le_bytes([payload_buf[0], payload_buf[1]]); + let cols = u16::from_le_bytes([payload_buf[2], payload_buf[3]]); + log::debug!("Resize: {}x{}", cols, rows); + if let Ok(master) = pty_master_clone.lock() { + let _ = master.resize(PtySize { + rows, + cols, + pixel_width: 0, + pixel_height: 0, + }); + } + } + } + _ => { + // Unknown type may indicate data corruption - stop to avoid parse errors + log::error!("Unknown message type: {}, terminating", msg_type); + break; + } + } + } + log::debug!("Input thread exiting"); + }); + + // Thread: Read from PTY, write to output pipe + let exiting_clone = exiting.clone(); + let output_thread = thread::spawn(move || { + let mut output_pipe = output_pipe; + let mut buf = vec![0u8; 4096]; + loop { + if exiting_clone.load(Ordering::SeqCst) { + break; + } + match pty_reader.read(&mut buf) { + Ok(0) => { + log::debug!("PTY EOF"); + break; + } + Ok(n) => { + if let Err(e) = output_pipe.write_all(&buf[..n]) { + log::error!("Output pipe write error: {}", e); + break; + } + if let Err(e) = output_pipe.flush() { + log::error!("Output pipe flush error: {}", e); + break; + } + } + Err(e) => { + if e.kind() != std::io::ErrorKind::WouldBlock { + log::error!("PTY read error: {}", e); + break; + } + thread::sleep(Duration::from_millis(10)); + } + } + } + log::debug!("Output thread exiting"); + }); + + // Wait for child process to exit + let exit_status = child.wait(); + log::info!("Shell exited: {:?}", exit_status); + + exiting.store(true, Ordering::SeqCst); + + // Wait for threads + let _ = input_thread.join(); + let _ = output_thread.join(); + + // pty_master will be dropped here, releasing PTY resources + drop(pty_master); + + log::info!("Terminal helper exiting"); + Ok(()) +} + +/// Read exactly `buf.len()` bytes from reader. +/// Returns Ok(true) if successful, Ok(false) on EOF, Err on error. +fn read_exact_or_eof(reader: &mut R, buf: &mut [u8]) -> std::io::Result { + let mut pos = 0; + while pos < buf.len() { + match reader.read(&mut buf[pos..]) { + Ok(0) => return Ok(false), // EOF + Ok(n) => pos += n, + Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue, + Err(e) => return Err(e), + } + } + Ok(true) +} + +/// Open a named pipe as a client. +/// `for_read`: true for reading (input pipe), false for writing (output pipe). +fn open_pipe(pipe_name: &str, for_read: bool) -> Result { + let wide_name: Vec = OsStr::new(pipe_name) + .encode_wide() + .chain(std::iter::once(0)) + .collect(); + + let access = if for_read { + FILE_GENERIC_READ.0 + } else { + FILE_GENERIC_WRITE.0 + }; + + let handle = unsafe { + CreateFileW( + PCWSTR::from_raw(wide_name.as_ptr()), + access, + FILE_SHARE_READ | FILE_SHARE_WRITE, + None, + OPEN_EXISTING, + FILE_FLAGS_AND_ATTRIBUTES(0), + None, + ) + }; + + match handle { + Ok(h) => Ok(unsafe { File::from_raw_handle(h.0 as _) }), + Err(e) => Err(anyhow!( + "Failed to open {} pipe '{}': {}", + if for_read { "input" } else { "output" }, + pipe_name, + e + )), + } +} diff --git a/src/server/terminal_service.rs b/src/server/terminal_service.rs index 194e41ef1..743f849c4 100644 --- a/src/server/terminal_service.rs +++ b/src/server/terminal_service.rs @@ -17,6 +17,15 @@ use std::{ time::{Duration, Instant}, }; +// Windows-specific imports from terminal_helper module +#[cfg(target_os = "windows")] +use super::terminal_helper::{ + create_named_pipe_server, encode_helper_message, encode_resize_message, + is_helper_process_running, launch_terminal_helper_with_token, wait_for_pipe_connection, + HelperProcessGuard, OwnedHandle, SendableHandle, WinCloseHandle, WinTerminateProcess, + WinWaitForSingleObject, MSG_TYPE_DATA, PIPE_CONNECTION_TIMEOUT_MS, WIN_WAIT_OBJECT_0, +}; + const MAX_OUTPUT_BUFFER_SIZE: usize = 1024 * 1024; // 1MB per terminal const MAX_BUFFER_LINES: usize = 10000; const MAX_SERVICES: usize = 100; // Maximum number of persistent terminal services @@ -53,28 +62,8 @@ pub fn generate_service_id() -> String { fn get_default_shell() -> String { #[cfg(target_os = "windows")] { - // Try PowerShell Core first (cross-platform version) - // Common installation paths for PowerShell Core - let pwsh_paths = [ - "pwsh.exe", - r"C:\Program Files\PowerShell\7\pwsh.exe", - r"C:\Program Files\PowerShell\6\pwsh.exe", - ]; - - for path in &pwsh_paths { - if std::path::Path::new(path).exists() { - return path.to_string(); - } - } - - // Try Windows PowerShell (should be available on all Windows systems) - let powershell_path = r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"; - if std::path::Path::new(powershell_path).exists() { - return powershell_path.to_string(); - } - - // Final fallback to cmd.exe - std::env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".to_string()) + // Use shared implementation from terminal_helper + super::terminal_helper::get_default_shell() } #[cfg(not(target_os = "windows"))] { @@ -280,7 +269,30 @@ pub fn get_terminal_session_count(include_zombie_tasks: bool) -> usize { c } -pub type UserToken = u64; +/// User token wrapper for cross-module use. +/// +/// # Design Note +/// On Windows, this type is defined in terminal_helper.rs and re-exported here. +/// On non-Windows platforms, it's defined here directly. +/// This design avoids circular dependencies while keeping the API consistent. +/// Both definitions MUST have identical public API (new, as_raw methods). +#[cfg(not(target_os = "windows"))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct UserToken(pub usize); + +#[cfg(not(target_os = "windows"))] +impl UserToken { + pub fn new(handle: usize) -> Self { + Self(handle) + } + + pub fn as_raw(&self) -> usize { + self.0 + } +} + +#[cfg(target_os = "windows")] +pub use super::terminal_helper::UserToken; #[derive(Clone)] pub struct TerminalService { @@ -458,6 +470,12 @@ pub struct TerminalSession { // Track if we've already sent the closed message closed_message_sent: bool, is_opened: bool, + // Helper mode: PTY is managed by helper process, communication via message protocol + #[cfg(target_os = "windows")] + is_helper_mode: bool, + // Handle to helper process for termination when session closes + #[cfg(target_os = "windows")] + helper_process_handle: Option, } impl TerminalSession { @@ -479,6 +497,10 @@ impl TerminalSession { cols, closed_message_sent: false, is_opened: false, + #[cfg(target_os = "windows")] + is_helper_mode: false, + #[cfg(target_os = "windows")] + helper_process_handle: None, } } @@ -497,14 +519,58 @@ impl TerminalSession { // Send a final newline to ensure the reader can read some data, and then exit. // This is required on Windows and Linux. // Although `self.pty_pair = None;` is called below, we can still send a final newline here. - if let Err(e) = input_tx.send(b"\r\n".to_vec()) { + #[cfg(target_os = "windows")] + let final_msg = if self.is_helper_mode { + encode_helper_message(MSG_TYPE_DATA, b"\r\n") + } else { + b"\r\n".to_vec() + }; + #[cfg(not(target_os = "windows"))] + let final_msg = b"\r\n".to_vec(); + + if let Err(e) = input_tx.send(final_msg) { log::warn!("Failed to send final newline to the terminal: {}", e); } drop(input_tx); } self.output_rx = None; - // 1. Windows + // CRITICAL: In helper mode, we must terminate the helper process BEFORE joining threads! + // The reader thread is blocking on output_pipe.read(), which only returns EOF when + // the helper process exits. If we try to join the reader thread first, we deadlock. + // + // Sequence for helper mode: + // 1. Signal exiting and close input channel (done above) + // 2. Terminate helper process (causes output pipe EOF) + // 3. Join reader thread (now unblocked due to EOF) + // 4. Join writer thread + #[cfg(target_os = "windows")] + if self.is_helper_mode { + if let Some(helper_handle) = self.helper_process_handle.take() { + let handle = helper_handle.as_raw(); + log::debug!("Helper mode: terminating helper process before joining threads..."); + + // Give helper a very short time to exit gracefully (it should detect pipe close) + // But don't wait too long - we need to unblock the reader thread + let wait_result = unsafe { WinWaitForSingleObject(handle, 100) }; + + if wait_result == WIN_WAIT_OBJECT_0 { + log::debug!("Helper process exited gracefully"); + } else { + // Force terminate to unblock reader thread + log::debug!("Force terminating helper process to unblock reader thread"); + unsafe { + let _ = WinTerminateProcess(handle, 0); + } + } + + unsafe { + let _ = WinCloseHandle(handle); + } + } + } + + // 1. Windows (non-helper mode) // `pty_pair` uses pipe. https://github.com/rustdesk-org/wezterm/blob/80174f8009f41565f0fa8c66dab90d4f9211ae16/pty/src/win/conpty.rs#L16 // `read()` may stuck at https://github.com/rustdesk-org/wezterm/blob/80174f8009f41565f0fa8c66dab90d4f9211ae16/filedescriptor/src/windows.rs#L345 // We can close the pipe to signal the reader thread to exit. @@ -747,6 +813,15 @@ impl TerminalServiceProxy { return Ok(Some(response)); } + // Windows with user_token: use helper process to run shell as the logged-in user + // This solves the ConPTY + CreateProcessAsUserW incompatibility issue where + // vim, Claude Code, and other TUI applications hang when ConPTY is created + // by SYSTEM service but shell runs as user via CreateProcessAsUserW. + #[cfg(target_os = "windows")] + if self.user_token.is_some() { + return self.handle_open_with_helper(service, open); + } + // Create new terminal session log::info!( "Creating new terminal {} for service: {}", @@ -774,12 +849,19 @@ impl TerminalServiceProxy { #[allow(unused_mut)] let mut cmd = CommandBuilder::new(&shell); - // Set `TERM` environment variable for macOS to ensure proper terminal behavior - // This fixes issues with control sequences (e.g., Delete/Backspace keys) - // macOS terminfo uses hex naming: '78' = 'x' for xterm entries + // macOS-specific terminal configuration + // 1. Use login shell (-l) to load user's shell profile (~/.zprofile, ~/.bash_profile) + // This ensures PATH includes Homebrew paths (/opt/homebrew/bin, /usr/local/bin) + // 2. Set TERM environment variable for proper terminal behavior + // This fixes issues with control sequences (e.g., Delete/Backspace keys) + // macOS terminfo uses hex naming: '78' = 'x' for xterm entries // Note: For Linux, `TERM` is set in src/platform/linux.rs try_start_server_() #[cfg(target_os = "macos")] { + // Start as login shell to load user environment (PATH, etc.) + cmd.arg("-l"); + log::debug!("Added -l flag for macOS login shell"); + let term = if std::path::Path::new("/usr/share/terminfo/78/xterm-256color").exists() { "xterm-256color" } else { @@ -789,10 +871,9 @@ impl TerminalServiceProxy { log::debug!("Set TERM={} for macOS PTY", term); } - #[cfg(target_os = "windows")] - if let Some(token) = &self.user_token { - cmd.set_user_token(*token as _); - } + // Note: On Windows with user_token, we use helper mode (handle_open_with_helper) + // which is dispatched earlier in this function. This code path is only reached + // when user_token is None (e.g., running directly as user, not as SYSTEM service). log::debug!("Spawning shell process..."); let child = pty_pair @@ -820,17 +901,6 @@ impl TerminalServiceProxy { let terminal_id = open.terminal_id; let writer_thread = thread::spawn(move || { let mut writer = writer; - // Write initial carriage return: - // 1. Windows requires at least one carriage return for `drop()` to work properly. - // Without this, the reader may fail to read the buffer after `input_tx.send(b"\r\n".to_vec()).ok();`. - // 2. This also refreshes the terminal interface on the controlling side (workaround for blank content on connect). - if let Err(e) = writer.write_all(b"\r") { - log::error!("Terminal {} initial write error: {}", terminal_id, e); - } else { - if let Err(e) = writer.flush() { - log::error!("Terminal {} initial flush error: {}", terminal_id, e); - } - } while let Ok(data) = input_rx.recv() { if let Err(e) = writer.write_all(&data) { log::error!("Terminal {} write error: {}", terminal_id, e); @@ -930,6 +1000,222 @@ impl TerminalServiceProxy { Ok(Some(response)) } + /// Windows-only: Open terminal using helper process pattern + /// This solves the ConPTY + CreateProcessAsUserW incompatibility issue. + /// The helper process runs as the logged-in user and creates ConPTY + shell, + /// communicating with this service via named pipes. + #[cfg(target_os = "windows")] + fn handle_open_with_helper( + &self, + service: &mut PersistentTerminalService, + open: &OpenTerminal, + ) -> Result> { + let mut response = TerminalResponse::new(); + + log::info!( + "Creating new terminal {} using helper process for service: {}", + open.terminal_id, + service.service_id + ); + + let mut session = + TerminalSession::new(open.terminal_id, open.rows as u16, open.cols as u16); + + // Generate unique pipe names for this terminal + let pipe_id = uuid::Uuid::new_v4(); + let input_pipe_name = format!(r"\\.\pipe\rustdesk_term_in_{}", pipe_id); + let output_pipe_name = format!(r"\\.\pipe\rustdesk_term_out_{}", pipe_id); + + log::debug!( + "Creating pipes: input={}, output={}", + input_pipe_name, + output_pipe_name + ); + + // Get user_token early - needed for both DACL creation and helper launch + let user_token = self + .user_token + .ok_or_else(|| anyhow!("user_token is required for helper mode"))?; + + // Create pipes (server side, don't wait for connection yet) + // input_pipe: service WRITES to this, helper READS from this + // output_pipe: service READS from this, helper WRITES to this + // Using OwnedHandle for RAII - handles are automatically closed on error + // Pass user_token to create restricted DACL (only SYSTEM + user can access) + let input_pipe_handle = OwnedHandle::new(create_named_pipe_server( + &input_pipe_name, + false, + user_token, + )?); + let output_pipe_handle = OwnedHandle::new(create_named_pipe_server( + &output_pipe_name, + true, + user_token, + )?); + + let helper_process_info = launch_terminal_helper_with_token( + user_token, + &input_pipe_name, + &output_pipe_name, + open.terminal_id, + open.rows as u16, + open.cols as u16, + )?; + + // Use HelperProcessGuard for RAII cleanup - terminates process on error + // Unlike OwnedHandle which only closes the handle, this guard ensures + // the helper process is terminated if pipe connection fails or other errors occur. + let helper_process_guard = + HelperProcessGuard::new(helper_process_info.handle, helper_process_info.pid); + let helper_pid = helper_process_guard.pid(); + + // Wait for helper to connect to pipes + // If this fails, HelperProcessGuard will terminate the helper process + let mut input_pipe = wait_for_pipe_connection( + input_pipe_handle, + &input_pipe_name, + PIPE_CONNECTION_TIMEOUT_MS, + )?; + let mut output_pipe = wait_for_pipe_connection( + output_pipe_handle, + &output_pipe_name, + PIPE_CONNECTION_TIMEOUT_MS, + )?; + + // Check if helper process is still running after pipe connection + // This provides early detection if helper crashed during startup + if !is_helper_process_running(helper_process_guard.as_raw()) { + return Err(anyhow!( + "Helper process (PID {}) exited unexpectedly after pipe connection", + helper_pid + )); + } + + // Disarm the guard and transfer ownership to session + // From this point, the session is responsible for terminating the helper + let helper_raw_handle = helper_process_guard.disarm(); + + // Use helper process PID for session tracking + // Note: This is the helper process PID, not the actual shell PID. + // The real shell runs inside the helper process but its PID is not exposed here. + // For process management (termination, status), the helper PID is what we need. + session.pid = helper_pid; + + // Create channels for input/output (same as direct PTY mode) + let (input_tx, input_rx) = mpsc::sync_channel::>(CHANNEL_BUFFER_SIZE); + let (output_tx, output_rx) = mpsc::sync_channel::>(CHANNEL_BUFFER_SIZE); + + // Spawn writer thread: reads from channel, writes to input pipe + let terminal_id = open.terminal_id; + let writer_thread = thread::spawn(move || { + while let Ok(data) = input_rx.recv() { + if let Err(e) = input_pipe.write_all(&data) { + log::error!("Terminal {} pipe write error: {}", terminal_id, e); + break; + } + if let Err(e) = input_pipe.flush() { + log::error!("Terminal {} pipe flush error: {}", terminal_id, e); + } + } + log::debug!( + "Terminal {} writer thread (helper mode) exiting", + terminal_id + ); + }); + + // Spawn reader thread: reads from output pipe, sends to channel + // Note: The output pipe was created with FILE_FLAG_OVERLAPPED for timeout support + // during ConnectNamedPipe. However, once converted to a File handle, reads are + // performed synchronously. The WouldBlock handling below is defensive but may + // not be triggered in practice since File::read() blocks until data is available. + let exiting = session.exiting.clone(); + let terminal_id = open.terminal_id; + let reader_thread = thread::spawn(move || { + let mut buf = vec![0u8; 4096]; + loop { + match output_pipe.read(&mut buf) { + Ok(0) => { + // EOF - helper process exited + log::debug!("Terminal {} helper output EOF", terminal_id); + break; + } + Ok(n) => { + if exiting.load(Ordering::SeqCst) { + break; + } + let data = buf[..n].to_vec(); + match output_tx.try_send(data) { + Ok(_) => {} + Err(mpsc::TrySendError::Full(_)) => { + log::debug!( + "Terminal {} output channel full, dropping data", + terminal_id + ); + } + Err(mpsc::TrySendError::Disconnected(_)) => { + log::debug!("Terminal {} output channel disconnected", terminal_id); + break; + } + } + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + // Defensive: WouldBlock is unlikely with synchronous File::read(), + // but handle it gracefully just in case. + if exiting.load(Ordering::SeqCst) { + break; + } + thread::sleep(Duration::from_millis(10)); + } + Err(e) => { + log::error!("Terminal {} pipe read error: {}", terminal_id, e); + break; + } + } + } + log::debug!( + "Terminal {} reader thread (helper mode) exiting", + terminal_id + ); + }); + + // In helper mode, we don't have pty_pair or child - helper manages those + session.pty_pair = None; + session.child = None; + session.input_tx = Some(input_tx); + session.output_rx = Some(output_rx); + session.reader_thread = Some(reader_thread); + session.writer_thread = Some(writer_thread); + session.is_opened = true; + session.is_helper_mode = true; + session.helper_process_handle = Some(SendableHandle::new(helper_raw_handle)); + + let mut opened = TerminalOpened::new(); + opened.terminal_id = open.terminal_id; + opened.success = true; + opened.message = "Terminal opened (helper mode)".to_string(); + opened.pid = session.pid; + opened.service_id = service.service_id.clone(); + if service.needs_session_sync { + if !service.sessions.is_empty() { + opened.persistent_sessions = service.sessions.keys().cloned().collect(); + } + service.needs_session_sync = false; + } + response.set_opened(opened); + + log::info!( + "Terminal {} opened successfully using helper process (PID {})", + open.terminal_id, + session.pid + ); + + service + .sessions + .insert(open.terminal_id, Arc::new(Mutex::new(session))); + + Ok(Some(response)) + } + fn handle_resize( &self, session: Option>>, @@ -941,18 +1227,50 @@ impl TerminalServiceProxy { session.rows = resize.rows as u16; session.cols = resize.cols as u16; - if let Some(pty_pair) = &session.pty_pair { - pty_pair.master.resize(PtySize { - rows: resize.rows as u16, - cols: resize.cols as u16, - pixel_width: 0, - pixel_height: 0, - })?; + // Windows: handle helper mode vs direct PTY mode + #[cfg(target_os = "windows")] + { + if session.is_helper_mode { + // Helper mode: send resize command via message protocol + if let Some(input_tx) = &session.input_tx { + let msg = encode_resize_message(resize.rows as u16, resize.cols as u16); + if let Err(e) = input_tx.send(msg) { + log::error!("Failed to send resize to helper: {}", e); + } + } else { + log::warn!( + "Terminal {} is in helper mode but input_tx is None, cannot send resize", + resize.terminal_id + ); + } + } else { + // Direct PTY mode + Self::resize_pty(&session, resize)?; + } + } + + // Non-Windows: always direct PTY mode + #[cfg(not(target_os = "windows"))] + { + Self::resize_pty(&session, resize)?; } } Ok(None) } + /// Resize PTY directly (used for non-helper mode) + fn resize_pty(session: &TerminalSession, resize: &ResizeTerminal) -> Result<()> { + if let Some(pty_pair) = &session.pty_pair { + pty_pair.master.resize(PtySize { + rows: resize.rows as u16, + cols: resize.cols as u16, + pixel_width: 0, + pixel_height: 0, + })?; + } + Ok(()) + } + fn handle_data( &self, session: Option>>, @@ -962,8 +1280,18 @@ impl TerminalServiceProxy { let mut session = session_arc.lock().unwrap(); session.update_activity(); if let Some(input_tx) = &session.input_tx { + // Encode data for helper mode or send raw for direct PTY mode + #[cfg(target_os = "windows")] + let msg = if session.is_helper_mode { + encode_helper_message(MSG_TYPE_DATA, &data.data) + } else { + data.data.to_vec() + }; + #[cfg(not(target_os = "windows"))] + let msg = data.data.to_vec(); + // Send data to writer thread - if let Err(e) = input_tx.send(data.data.to_vec()) { + if let Err(e) = input_tx.send(msg) { log::error!( "Failed to send data to terminal {}: {}", data.terminal_id, From 4d3ccc62e8686a7108e4393ae612aa60a1337159 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:08:15 +0800 Subject: [PATCH 066/277] fix(file transfer): perm on "access-mode" (#13971) Signed-off-by: fufesou --- src/ui_cm_interface.rs | 5 +---- src/ui_interface.rs | 23 +++++++++++++++++++---- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index d1c1d21ef..d6792c111 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -772,10 +772,7 @@ impl IpcTaskRunner { #[tokio::main(flavor = "current_thread")] pub async fn start_ipc(cm: ConnectionManager) { #[cfg(target_os = "windows")] - ContextSend::enable(option2bool( - OPTION_ENABLE_FILE_TRANSFER, - &Config::get_option(OPTION_ENABLE_FILE_TRANSFER), - )); + ContextSend::enable(crate::Connection::permission(OPTION_ENABLE_FILE_TRANSFER)); match ipc::new_listener("_cm").await { Ok(mut incoming) => { while let Some(result) = incoming.next().await { diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 516e4fede..549337aea 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -1167,6 +1167,8 @@ async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver Some(true), + "view" => Some(false), + _ => None, + }; + let enabled = access_mode_enabled.unwrap_or(config::option2bool(OPTION_ENABLE_FILE_TRANSFER, &ft)); + clipboard::ContextSend::enable(enabled); + enable_file_transfer = ft; + access_mode = am; } } } From 3a9084006f769308645aa4d1dc3ecf79766c180b Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 9 Jan 2026 00:21:28 +0800 Subject: [PATCH 067/277] Allow configuring remote control permissions for different users (#13974) Signed-off-by: 21pages --- flutter/lib/common.dart | 15 ++++- src/common.rs | 22 ++++++ src/flutter_ffi.rs | 7 ++ src/ipc.rs | 34 +++++++++- src/rendezvous_mediator.rs | 70 ++++++++++++++++--- src/server.rs | 47 ++++++++++--- src/server/connection.rs | 134 +++++++++++++++++++++++++++++++++---- src/ui.rs | 10 +++ src/ui/index.tis | 11 ++- src/ui_cm_interface.rs | 9 ++- src/ui_interface.rs | 54 ++++++++------- 11 files changed, 353 insertions(+), 60 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index b4c9c6e82..bd7948de0 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -3039,10 +3039,21 @@ Future start_service(bool is_start) async { } Future canBeBlocked() async { - var access_mode = await bind.mainGetOption(key: kOptionAccessMode); + // First check control permission + final controlPermission = await bind.mainGetCommon( + key: "is-remote-modify-enabled-by-control-permissions"); + if (controlPermission == "true") { + return false; + } else if (controlPermission == "false") { + return true; + } + + // Check local settings + var accessMode = await bind.mainGetOption(key: kOptionAccessMode); + var isCustomAccessMode = accessMode != 'full' && accessMode != 'view'; var option = option2bool(kOptionAllowRemoteConfigModification, await bind.mainGetOption(key: kOptionAllowRemoteConfigModification)); - return access_mode == 'view' || (access_mode.isEmpty && !option); + return accessMode == 'view' || (isCustomAccessMode && !option); } // to-do: web not implemented diff --git a/src/common.rs b/src/common.rs index 0dc944d83..66a12994d 100644 --- a/src/common.rs +++ b/src/common.rs @@ -2277,6 +2277,28 @@ pub fn str2color(s: &str, alpha: u8) -> u32 { (alpha as u32) << 24 | rgb } +/// Check control permission state from a u64 bitmap. +/// Each permission uses 2 bits: 0 = not set, 1 = disable, 2 = enable, 3 = invalid (treated as not set) +/// Returns: Some(true) = enabled, Some(false) = disabled, None = not set or invalid +pub fn get_control_permission( + permissions: u64, + permission: hbb_common::rendezvous_proto::control_permissions::Permission, +) -> Option { + use hbb_common::protobuf::Enum; + let index = permission.value(); + if index >= 0 && index < 32 { + let shift = index * 2; + let value = (permissions >> shift) & 0b11; + match value { + 1 => Some(false), // disable + 2 => Some(true), // enable + _ => None, // 0 = not set, 3 = invalid + } + } else { + None + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index ff74b8b79..f2d3e34ef 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -2600,6 +2600,13 @@ pub fn main_get_common(key: String) -> String { return false.to_string(); } else if key == "transfer-job-id" { return hbb_common::fs::get_next_job_id().to_string(); + } else if key == "is-remote-modify-enabled-by-control-permissions" { + return match is_remote_modify_enabled_by_control_permissions() { + Some(true) => "true", + Some(false) => "false", + None => "", + } + .to_string(); } else { if key.starts_with("download-data-") { let id = key.replace("download-data-", ""); diff --git a/src/ipc.rs b/src/ipc.rs index e5f163c2e..a5d27ba8a 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -23,7 +23,11 @@ pub use clipboard::ClipboardFile; use hbb_common::{ allow_err, bail, bytes, bytes_codec::BytesCodec, - config::{self, keys::OPTION_ALLOW_WEBSOCKET, Config, Config2}, + config::{ + self, + keys::{self, OPTION_ALLOW_WEBSOCKET}, + Config, Config2, + }, futures::StreamExt as _, futures_util::sink::SinkExt, log, password_security as password, timeout, @@ -384,6 +388,9 @@ pub enum Data { SocksWs(Option, String)>>), #[cfg(not(any(target_os = "android", target_os = "ios")))] Whiteboard((String, crate::whiteboard::CustomEvent)), + ControlPermissionsRemoteModify(Option), + #[cfg(target_os = "windows")] + FileTransferEnabledState(Option), } #[tokio::main(flavor = "current_thread")] @@ -862,6 +869,31 @@ async fn handle(data: Data, stream: &mut Connection) { // Port forward session count is only a get value. } }, + Data::ControlPermissionsRemoteModify(_) => { + use hbb_common::rendezvous_proto::control_permissions::Permission; + let state = + crate::server::get_control_permission_state(Permission::remote_modify, true); + allow_err!( + stream + .send(&Data::ControlPermissionsRemoteModify(state)) + .await + ); + } + #[cfg(target_os = "windows")] + Data::FileTransferEnabledState(_) => { + use hbb_common::rendezvous_proto::control_permissions::Permission; + let state = crate::server::get_control_permission_state(Permission::file, false); + let enabled = state.unwrap_or_else(|| { + crate::server::Connection::is_permission_enabled_locally( + config::keys::OPTION_ENABLE_FILE_TRANSFER, + ) + }); + allow_err!( + stream + .send(&Data::FileTransferEnabledState(Some(enabled))) + .await + ); + } _ => {} } } diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index e17920c8a..5d26d3389 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -427,6 +427,7 @@ impl RendezvousMediator { rr.secure, false, Default::default(), + rr.control_permissions.clone().into_option(), ) .await } @@ -440,6 +441,7 @@ impl RendezvousMediator { secure: bool, initiate: bool, socket_addr_v6: bytes::Bytes, + control_permissions: Option, ) -> ResultType<()> { let peer_addr = AddrMangle::decode(&socket_addr); log::info!( @@ -473,6 +475,7 @@ impl RendezvousMediator { peer_addr, secure, is_ipv4(&self.addr), + control_permissions, ) .await; Ok(()) @@ -491,7 +494,13 @@ impl RendezvousMediator { let relay = use_ws() || Config::is_proxy(); let mut socket_addr_v6 = Default::default(); if peer_addr_v6.port() > 0 && !relay { - socket_addr_v6 = start_ipv6(peer_addr_v6, addr, server.clone()).await; + socket_addr_v6 = start_ipv6( + peer_addr_v6, + addr, + server.clone(), + fla.control_permissions.clone().into_option(), + ) + .await; } if is_ipv4(&self.addr) && !relay && !config::is_disable_tcp_listen() { if let Err(err) = self @@ -517,6 +526,7 @@ impl RendezvousMediator { true, true, socket_addr_v6, + fla.control_permissions.into_option(), ) .await } @@ -547,7 +557,14 @@ impl RendezvousMediator { }); let bytes = msg_out.write_to_bytes()?; socket.send_raw(bytes).await?; - crate::accept_connection(server.clone(), socket, peer_addr, true).await; + crate::accept_connection( + server.clone(), + socket, + peer_addr, + true, + fla.control_permissions.into_option(), + ) + .await; Ok(()) } @@ -562,8 +579,15 @@ impl RendezvousMediator { let peer_addr_v6 = hbb_common::AddrMangle::decode(&ph.socket_addr_v6); let relay = use_ws() || Config::is_proxy() || ph.force_relay; let mut socket_addr_v6 = Default::default(); + let control_permissions = ph.control_permissions.into_option(); if peer_addr_v6.port() > 0 && !relay { - socket_addr_v6 = start_ipv6(peer_addr_v6, peer_addr, server.clone()).await; + socket_addr_v6 = start_ipv6( + peer_addr_v6, + peer_addr, + server.clone(), + control_permissions.clone(), + ) + .await; } let relay_server = self.get_relay_server(ph.relay_server); // for ensure, websocket go relay directly @@ -582,6 +606,7 @@ impl RendezvousMediator { true, true, socket_addr_v6.clone(), + control_permissions, ) .await; } @@ -598,7 +623,8 @@ impl RendezvousMediator { }; if ph.udp_port > 0 { peer_addr.set_port(ph.udp_port as u16); - self.punch_udp_hole(peer_addr, server, msg_punch).await?; + self.punch_udp_hole(peer_addr, server, msg_punch, control_permissions) + .await?; return Ok(()); } log::debug!("Punch tcp hole to {:?}", peer_addr); @@ -614,7 +640,8 @@ impl RendezvousMediator { msg_out.set_punch_hole_sent(msg_punch); let bytes = msg_out.write_to_bytes()?; socket.send_raw(bytes).await?; - crate::accept_connection(server.clone(), socket, peer_addr, true).await; + crate::accept_connection(server.clone(), socket, peer_addr, true, control_permissions) + .await; Ok(()) } @@ -623,6 +650,7 @@ impl RendezvousMediator { peer_addr: SocketAddr, server: ServerPtr, msg_punch: PunchHoleSent, + control_permissions: Option, ) -> ResultType<()> { let mut msg_out = Message::new(); msg_out.set_punch_hole_sent(msg_punch); @@ -637,7 +665,14 @@ impl RendezvousMediator { socket.send_to(&data, addr).await.ok(); } }); - udp_nat_listen(socket_cloned.clone(), peer_addr, peer_addr, server).await?; + udp_nat_listen( + socket_cloned.clone(), + peer_addr, + peer_addr, + server, + control_permissions, + ) + .await?; Ok(()) } @@ -778,6 +813,7 @@ async fn direct_server(server: ServerPtr) { hbb_common::Stream::from(stream, local_addr), addr, false, + None, // Direct connections don't have control_permissions ) .await ); @@ -809,12 +845,22 @@ async fn start_ipv6( peer_addr_v6: SocketAddr, peer_addr_v4: SocketAddr, server: ServerPtr, + control_permissions: Option, ) -> bytes::Bytes { crate::test_ipv6().await; if let Some((socket, local_addr_v6)) = crate::get_ipv6_socket().await { let server = server.clone(); tokio::spawn(async move { - allow_err!(udp_nat_listen(socket.clone(), peer_addr_v6, peer_addr_v4, server).await); + allow_err!( + udp_nat_listen( + socket.clone(), + peer_addr_v6, + peer_addr_v4, + server, + control_permissions + ) + .await + ); }); return local_addr_v6; } @@ -826,6 +872,7 @@ async fn udp_nat_listen( peer_addr: SocketAddr, peer_addr_v4: SocketAddr, server: ServerPtr, + control_permissions: Option, ) -> ResultType<()> { let tm = Instant::now(); let socket_cloned = socket.clone(); @@ -838,7 +885,14 @@ async fn udp_nat_listen( res, ) .await?; - crate::server::create_tcp_connection(server, stream.1, peer_addr_v4, true).await?; + crate::server::create_tcp_connection( + server, + stream.1, + peer_addr_v4, + true, + control_permissions, + ) + .await?; Ok(()) }; func.await.map_err(|e: anyhow::Error| { diff --git a/src/server.rs b/src/server.rs index 9d2e4b804..5dc504fe9 100644 --- a/src/server.rs +++ b/src/server.rs @@ -154,18 +154,30 @@ pub fn new() -> ServerPtr { Arc::new(RwLock::new(server)) } -async fn accept_connection_(server: ServerPtr, socket: Stream, secure: bool) -> ResultType<()> { +async fn accept_connection_( + server: ServerPtr, + socket: Stream, + secure: bool, + control_permissions: Option, +) -> ResultType<()> { let local_addr = socket.local_addr(); drop(socket); // even we drop socket, below still may fail if not use reuse_addr, // there is TIME_WAIT before socket really released, so sometimes we - // see “Only one usage of each socket address is normally permitted” on windows sometimes, + // see "Only one usage of each socket address is normally permitted" on windows sometimes, let listener = new_listener(local_addr, true).await?; log::info!("Server listening on: {}", &listener.local_addr()?); if let Ok((stream, addr)) = timeout(CONNECT_TIMEOUT, listener.accept()).await? { stream.set_nodelay(true).ok(); let stream_addr = stream.local_addr()?; - create_tcp_connection(server, Stream::from(stream, stream_addr), addr, secure).await?; + create_tcp_connection( + server, + Stream::from(stream, stream_addr), + addr, + secure, + control_permissions, + ) + .await?; } Ok(()) } @@ -175,6 +187,7 @@ pub async fn create_tcp_connection( stream: Stream, addr: SocketAddr, secure: bool, + control_permissions: Option, ) -> ResultType<()> { let mut stream = stream; let id = server.write().unwrap().get_new_id(); @@ -242,7 +255,14 @@ pub async fn create_tcp_connection( } log::info!("wake up macos"); } - Connection::start(addr, stream, id, Arc::downgrade(&server)).await; + Connection::start( + addr, + stream, + id, + Arc::downgrade(&server), + control_permissions, + ) + .await; Ok(()) } @@ -251,8 +271,9 @@ pub async fn accept_connection( socket: Stream, peer_addr: SocketAddr, secure: bool, + control_permissions: Option, ) { - if let Err(err) = accept_connection_(server, socket, secure).await { + if let Err(err) = accept_connection_(server, socket, secure, control_permissions).await { log::warn!("Failed to accept connection from {}: {}", peer_addr, err); } } @@ -264,9 +285,18 @@ pub async fn create_relay_connection( peer_addr: SocketAddr, secure: bool, ipv4: bool, + control_permissions: Option, ) { - if let Err(err) = - create_relay_connection_(server, relay_server, uuid.clone(), peer_addr, secure, ipv4).await + if let Err(err) = create_relay_connection_( + server, + relay_server, + uuid.clone(), + peer_addr, + secure, + ipv4, + control_permissions, + ) + .await { log::error!( "Failed to create relay connection for {} with uuid {}: {}", @@ -284,6 +314,7 @@ async fn create_relay_connection_( peer_addr: SocketAddr, secure: bool, ipv4: bool, + control_permissions: Option, ) -> ResultType<()> { let mut stream = socket_client::connect_tcp( socket_client::ipv4_to_ipv6(crate::check_port(relay_server, RELAY_PORT), ipv4), @@ -298,7 +329,7 @@ async fn create_relay_connection_( ..Default::default() }); stream.send(&msg_out).await?; - create_tcp_connection(server, stream, peer_addr, secure).await?; + create_tcp_connection(server, stream, peer_addr, secure, control_permissions).await?; Ok(()) } diff --git a/src/server/connection.rs b/src/server/connection.rs index ee8cad591..1e7758887 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -71,6 +71,7 @@ lazy_static::lazy_static! { static ref SESSIONS: Arc::>> = Default::default(); static ref ALIVE_CONNS: Arc::>> = Default::default(); pub static ref AUTHED_CONNS: Arc::>> = Default::default(); + pub static ref CONTROL_PERMISSIONS_ARRAY: Arc::>> = Default::default(); static ref SWITCH_SIDES_UUID: Arc::>> = Default::default(); static ref WAKELOCK_SENDER: Arc::>> = Arc::new(Mutex::new(start_wakelock_thread())); } @@ -226,6 +227,7 @@ pub struct Connection { restart: bool, recording: bool, block_input: bool, + control_permissions: Option, last_test_delay: Option, network_delay: u32, lock_after_session_end: bool, @@ -349,8 +351,14 @@ impl Connection { stream: super::Stream, id: i32, server: super::ServerPtrWeak, + control_permissions: Option, ) { + // Android is not supported yet, so we always set control_permissions to None. + #[cfg(target_os = "android")] + let control_permissions = None; let _raii_id = raii::ConnectionID::new(id); + let _raii_control_permissions_id = + raii::ControlPermissionsID::new(id, &control_permissions); let hash = Hash { salt: Config::get_salt(), challenge: Config::get_auto_password(6), @@ -401,14 +409,15 @@ impl Connection { port_forward_address: "".to_owned(), tx_to_cm, authorized: false, - keyboard: Connection::permission("enable-keyboard"), - clipboard: Connection::permission("enable-clipboard"), - audio: Connection::permission("enable-audio"), + keyboard: Self::permission(keys::OPTION_ENABLE_KEYBOARD, &control_permissions), + clipboard: Self::permission(keys::OPTION_ENABLE_CLIPBOARD, &control_permissions), + audio: Self::permission(keys::OPTION_ENABLE_AUDIO, &control_permissions), // to-do: make sure is the option correct here - file: Connection::permission(keys::OPTION_ENABLE_FILE_TRANSFER), - restart: Connection::permission("enable-remote-restart"), - recording: Connection::permission("enable-record-session"), - block_input: Connection::permission("enable-block-input"), + file: Self::permission(keys::OPTION_ENABLE_FILE_TRANSFER, &control_permissions), + restart: Self::permission(keys::OPTION_ENABLE_REMOTE_RESTART, &control_permissions), + recording: Self::permission(keys::OPTION_ENABLE_RECORD_SESSION, &control_permissions), + block_input: Self::permission(keys::OPTION_ENABLE_BLOCK_INPUT, &control_permissions), + control_permissions, last_test_delay: None, network_delay: 0, lock_after_session_end: false, @@ -885,7 +894,7 @@ impl Connection { match data { #[cfg(all(target_os = "windows", feature = "flutter"))] ipc::Data::PrinterData(data) => { - if config::Config::get_bool_option(config::keys::OPTION_ENABLE_REMOTE_PRINTER) { + if Self::permission(keys::OPTION_ENABLE_REMOTE_PRINTER, &conn.control_permissions) { conn.send_printer_request(data).await; } else { conn.send_remote_printing_disallowed().await; @@ -1942,7 +1951,8 @@ impl Connection { false } - pub fn permission(enable_prefix_option: &str) -> bool { + #[inline] + pub fn is_permission_enabled_locally(enable_prefix_option: &str) -> bool { #[cfg(feature = "flutter")] #[cfg(not(any(target_os = "android", target_os = "ios")))] { @@ -1959,6 +1969,37 @@ impl Connection { ) } + fn permission( + enable_prefix_option: &str, + control_permissions: &Option, + ) -> bool { + use hbb_common::rendezvous_proto::control_permissions::Permission; + if let Some(control_permissions) = control_permissions { + let permission = match enable_prefix_option { + keys::OPTION_ENABLE_KEYBOARD => Some(Permission::keyboard), + keys::OPTION_ENABLE_REMOTE_PRINTER => Some(Permission::remote_printer), + keys::OPTION_ENABLE_CLIPBOARD => Some(Permission::clipboard), + keys::OPTION_ENABLE_FILE_TRANSFER => Some(Permission::file), + keys::OPTION_ENABLE_AUDIO => Some(Permission::audio), + keys::OPTION_ENABLE_CAMERA => Some(Permission::camera), + keys::OPTION_ENABLE_TERMINAL => Some(Permission::terminal), + keys::OPTION_ENABLE_TUNNEL => Some(Permission::tunnel), + keys::OPTION_ENABLE_REMOTE_RESTART => Some(Permission::restart), + keys::OPTION_ENABLE_RECORD_SESSION => Some(Permission::recording), + keys::OPTION_ENABLE_BLOCK_INPUT => Some(Permission::block_input), + _ => None, + }; + if let Some(permission) = permission { + if let Some(enabled) = + crate::get_control_permission(control_permissions.permissions, permission) + { + return enabled; + } + } + } + Self::is_permission_enabled_locally(enable_prefix_option) + } + fn update_codec_on_login(&self) { use scrap::codec::{Encoder, EncodingUpdate::*}; if let Some(o) = self.lr.clone().option.as_ref() { @@ -2054,7 +2095,10 @@ impl Connection { } match lr.union { Some(login_request::Union::FileTransfer(ft)) => { - if !Connection::permission(keys::OPTION_ENABLE_FILE_TRANSFER) { + if !Self::permission( + keys::OPTION_ENABLE_FILE_TRANSFER, + &self.control_permissions, + ) { self.send_login_error("No permission of file transfer") .await; sleep(1.).await; @@ -2063,7 +2107,7 @@ impl Connection { self.file_transfer = Some((ft.dir, ft.show_hidden)); } Some(login_request::Union::ViewCamera(_vc)) => { - if !Connection::permission(keys::OPTION_ENABLE_CAMERA) { + if !Self::permission(keys::OPTION_ENABLE_CAMERA, &self.control_permissions) { self.send_login_error("No permission of viewing camera") .await; sleep(1.).await; @@ -2072,7 +2116,7 @@ impl Connection { self.view_camera = true; } Some(login_request::Union::Terminal(terminal)) => { - if !Connection::permission(keys::OPTION_ENABLE_TERMINAL) { + if !Self::permission(keys::OPTION_ENABLE_TERMINAL, &self.control_permissions) { self.send_login_error("No permission of terminal").await; sleep(1.).await; return false; @@ -2120,7 +2164,7 @@ impl Connection { } } Some(login_request::Union::PortForward(mut pf)) => { - if !Connection::permission("enable-tunnel") { + if !Self::permission(keys::OPTION_ENABLE_TUNNEL, &self.control_permissions) { self.send_login_error("No permission of IP tunneling").await; sleep(1.).await; return false; @@ -5167,6 +5211,41 @@ impl Retina { } } +/// Get control permission state from CONTROL_PERMISSIONS_ARRAY. +/// Returns: Some(false) if any disable, Some(true) if any enable (and no disable), None if not set. +pub fn get_control_permission_state( + permission: hbb_common::rendezvous_proto::control_permissions::Permission, + disable_if_has_disabled: bool, +) -> Option { + let control_permissions = CONTROL_PERMISSIONS_ARRAY.lock().unwrap(); + let mut has_enable = false; + let mut has_disable = false; + for (_, cp) in control_permissions.iter() { + match crate::get_control_permission(cp.permissions, permission) { + Some(false) => has_disable = true, + Some(true) => has_enable = true, + None => {} + } + } + if disable_if_has_disabled { + if has_disable { + Some(false) + } else if has_enable { + Some(true) + } else { + None + } + } else { + if has_enable { + Some(true) + } else if has_disable { + Some(false) + } else { + None + } + } +} + pub struct AuthedConn { pub conn_id: i32, pub conn_type: AuthConnType, @@ -5178,6 +5257,7 @@ pub struct AuthedConn { mod raii { // ALIVE_CONNS: all connections, including unauthorized connections // AUTHED_CONNS: all authorized connections + // CONTROL_PERMISSIONS_ARRAY: all non-None control permissions use super::*; pub struct ConnectionID(i32); @@ -5368,6 +5448,34 @@ mod raii { } } } + + pub struct ControlPermissionsID { + id: i32, + control_permissions: Option, + } + + impl Drop for ControlPermissionsID { + fn drop(&mut self) { + if self.control_permissions.is_some() { + let mut lock = CONTROL_PERMISSIONS_ARRAY.lock().unwrap(); + lock.retain(|(conn_id, _)| *conn_id != self.id); + } + } + } + impl ControlPermissionsID { + pub fn new(id: i32, control_permissions: &Option) -> Self { + if let Some(s) = control_permissions { + CONTROL_PERMISSIONS_ARRAY + .lock() + .unwrap() + .push((id, s.clone())); + } + Self { + id, + control_permissions: control_permissions.clone(), + } + } + } } mod test { diff --git a/src/ui.rs b/src/ui.rs index 2a0f6e918..fc59cffd2 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -699,6 +699,15 @@ impl UI { fn get_builtin_option(&self, key: String) -> String { crate::ui_interface::get_builtin_option(&key) } + + fn is_remote_modify_enabled_by_control_permissions(&self) -> String { + match crate::ui_interface::is_remote_modify_enabled_by_control_permissions() { + Some(true) => "true", + Some(false) => "false", + None => "", + } + .to_string() + } } impl sciter::EventHandler for UI { @@ -801,6 +810,7 @@ impl sciter::EventHandler for UI { fn verify_login(String, String); fn is_option_fixed(String); fn get_builtin_option(String); + fn is_remote_modify_enabled_by_control_permissions(); } } diff --git a/src/ui/index.tis b/src/ui/index.tis index 20cbb7ba2..8dd4da3d4 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -1396,7 +1396,16 @@ function self.onMouse(evt) { } function check_if_overlay() { - if (handler.get_option('allow-remote-config-modification') != 'Y') { + var enabled; + var is_enabled_by_control_permissions = handler.is_remote_modify_enabled_by_control_permissions(); + if (is_enabled_by_control_permissions == "true") { + enabled = true; + } else if (is_enabled_by_control_permissions == "false") { + enabled = false; + } else { + enabled = handler.get_option('allow-remote-config-modification') == 'Y'; + } + if (!enabled) { var time0 = getTime(); handler.check_mouse_time(); self.timer(120ms, function() { diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index d6792c111..4e688429f 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -772,7 +772,14 @@ impl IpcTaskRunner { #[tokio::main(flavor = "current_thread")] pub async fn start_ipc(cm: ConnectionManager) { #[cfg(target_os = "windows")] - ContextSend::enable(crate::Connection::permission(OPTION_ENABLE_FILE_TRANSFER)); + { + let enabled = crate::Connection::is_permission_enabled_locally(OPTION_ENABLE_FILE_TRANSFER); + let mut lock = crate::ui_interface::IS_FILE_TRANSFER_ENABLED + .lock() + .unwrap(); + ContextSend::enable(enabled); + *lock = Some(enabled); + } match ipc::new_listener("_cm").await { Ok(mut incoming) => { while let Some(result) = incoming.next().await { diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 549337aea..c5f158c9d 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -69,6 +69,7 @@ lazy_static::lazy_static! { static ref ASYNC_JOB_STATUS : Arc> = Default::default(); static ref ASYNC_HTTP_STATUS : Arc>> = Arc::new(Mutex::new(HashMap::new())); static ref TEMPORARY_PASSWD : Arc> = Arc::new(Mutex::new("".to_owned())); + static ref IS_REMOTE_MODIFY_ENABLED_BY_CONTROL_PERMISSIONS : Arc>> = Arc::new(Mutex::new(None)); } #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -79,6 +80,11 @@ lazy_static::lazy_static! { static ref CHILDREN : Children = Default::default(); } +#[cfg(target_os = "windows")] +lazy_static::lazy_static! { + pub static ref IS_FILE_TRANSFER_ENABLED: Arc>> = Arc::new(Mutex::new(None)); +} + const INIT_ASYNC_JOB_STATUS: &str = " "; #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] @@ -1166,10 +1172,6 @@ async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver { *OPTIONS.lock().unwrap() = v; *OPTION_SYNCED.lock().unwrap() = true; - - #[cfg(target_os = "windows")] - { - let (ft, am) = { - let lock = OPTIONS.lock().unwrap(); - ( - lock.get(OPTION_ENABLE_FILE_TRANSFER).map(|x| x.to_string()).unwrap_or_default(), - lock.get(OPTION_ACCESS_MODE).map(|x| x.to_string()).unwrap_or_default(), - ) - }; - if ft != enable_file_transfer || am != access_mode { - let access_mode_enabled = match am.as_str() { - "full" => Some(true), - "view" => Some(false), - _ => None, - }; - let enabled = access_mode_enabled.unwrap_or(config::option2bool(OPTION_ENABLE_FILE_TRANSFER, &ft)); - clipboard::ContextSend::enable(enabled); - enable_file_transfer = ft; - access_mode = am; - } - } } Ok(Some(ipc::Data::Config((name, Some(value))))) => { if name == "id" { @@ -1251,6 +1231,19 @@ async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver { + *IS_REMOTE_MODIFY_ENABLED_BY_CONTROL_PERMISSIONS.lock().unwrap() = v; + } + #[cfg(target_os = "windows")] + Ok(Some(ipc::Data::FileTransferEnabledState(v))) => { + if let Some(enabled) = v { + let mut lock = IS_FILE_TRANSFER_ENABLED.lock().unwrap(); + if *lock != v { + clipboard::ContextSend::enable(enabled); + *lock = v; + } + } + } _ => {} } } @@ -1264,6 +1257,9 @@ async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver usize { hbb_common::config::ENCRYPT_MAX_LEN } + +pub fn is_remote_modify_enabled_by_control_permissions() -> Option { + *IS_REMOTE_MODIFY_ENABLED_BY_CONTROL_PERMISSIONS + .lock() + .unwrap() +} From 998b75856da4199ac009ce4135b4fba1b48099e6 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 9 Jan 2026 10:03:14 +0800 Subject: [PATCH 068/277] feat: Add relative mouse mode (#13928) * feat: Add relative mouse mode - Add "Relative Mouse Mode" toggle in desktop toolbar and bind to InputModel - Implement relative mouse movement path: Flutter pointer deltas -> `type: move_relative` -> new `MOUSE_TYPE_MOVE_RELATIVE` in Rust - In server input service, simulate relative movement via Enigo and keep latest cursor position in sync - Track pointer-lock center in Flutter (local widget + screen coordinates) and re-center OS cursor after each relative move - Update pointer-lock center on window move/resize/restore/maximize and when remote display geometry changes - Hide local cursor when relative mouse mode is active (both Flutter cursor and OS cursor), restore on leave/disable - On Windows, clip OS cursor to the window rect while in relative mode and release clip when leaving/turning off - Implement platform helpers: `get_cursor_pos`, `set_cursor_pos`, `show_cursor`, `clip_cursor` (no-op clip/hide on Linux for now) - Add keyboard shortcut Ctrl+Alt+Shift+M to toggle relative mode (enabled by default, works on all platforms) - Remove `enable-relative-mouse-shortcut` config option - shortcut is now always available when keyboard permission is granted - Handle window blur/focus/minimize events to properly release/restore cursor constraints - Add MOUSE_TYPE_MASK constant and unit tests for mouse event constants Note: Relative mouse mode state is NOT persisted to config (session-only). Note: On Linux, show_cursor and clip_cursor are no-ops; cursor hiding is handled by Flutter side. Signed-off-by: fufesou * feat(mouse): relative mouse mode, exit hint Signed-off-by: fufesou * refact(relative mouse): shortcut Signed-off-by: fufesou --------- Signed-off-by: fufesou --- .github/workflows/flutter-build.yml | 2 +- .github/workflows/playground.yml | 2 +- .github/workflows/winget.yml | 4 +- Cargo.lock | 4 +- Cargo.toml | 2 +- appimage/AppImageBuilder-aarch64.yml | 2 +- appimage/AppImageBuilder-x86_64.yml | 2 +- flutter/lib/common.dart | 26 +- flutter/lib/common/widgets/remote_input.dart | 17 +- flutter/lib/common/widgets/toolbar.dart | 29 + flutter/lib/consts.dart | 27 + flutter/lib/desktop/pages/remote_page.dart | 242 +++- .../lib/desktop/pages/remote_tab_page.dart | 76 +- .../lib/desktop/widgets/tabbar_widget.dart | 1 - flutter/lib/mobile/pages/remote_page.dart | 5 +- .../widgets/floating_mouse_widgets.dart | 43 +- flutter/lib/mobile/widgets/gesture_help.dart | 63 +- flutter/lib/models/input_model.dart | 246 +++- flutter/lib/models/model.dart | 67 +- flutter/lib/models/relative_mouse_model.dart | 1061 +++++++++++++++++ flutter/lib/models/state_model.dart | 6 +- .../lib/utils/relative_mouse_accumulator.dart | 58 + flutter/lib/web/bridge.dart | 14 + flutter/macos/Runner/MainFlutterWindow.swift | 133 ++- flutter/pubspec.yaml | 2 +- libs/enigo/src/macos/macos_impl.rs | 105 +- libs/portable/Cargo.toml | 2 +- res/PKGBUILD | 2 +- res/rpm-flutter-suse.spec | 2 +- res/rpm-flutter.spec | 2 +- res/rpm.spec | 2 +- src/common.rs | 59 + src/flutter_ffi.rs | 152 +++ src/keyboard.rs | 171 ++- src/lang/ar.rs | 6 + src/lang/be.rs | 6 + src/lang/bg.rs | 6 + src/lang/ca.rs | 6 + src/lang/cn.rs | 6 + src/lang/cs.rs | 6 + src/lang/da.rs | 6 + src/lang/de.rs | 6 + src/lang/el.rs | 6 + src/lang/en.rs | 5 + src/lang/eo.rs | 6 + src/lang/es.rs | 6 + src/lang/et.rs | 6 + src/lang/eu.rs | 6 + src/lang/fa.rs | 6 + src/lang/fi.rs | 6 + src/lang/fr.rs | 6 + src/lang/ge.rs | 6 + src/lang/he.rs | 6 + src/lang/hr.rs | 6 + src/lang/hu.rs | 6 + src/lang/id.rs | 6 + src/lang/it.rs | 6 + src/lang/ja.rs | 6 + src/lang/ko.rs | 6 + src/lang/kz.rs | 6 + src/lang/lt.rs | 6 + src/lang/lv.rs | 6 + src/lang/nb.rs | 6 + src/lang/nl.rs | 6 + src/lang/pl.rs | 6 + src/lang/pt_PT.rs | 6 + src/lang/ptbr.rs | 6 + src/lang/ro.rs | 6 + src/lang/ru.rs | 6 + src/lang/sc.rs | 6 + src/lang/sk.rs | 6 + src/lang/sl.rs | 6 + src/lang/sq.rs | 6 + src/lang/sr.rs | 6 + src/lang/sv.rs | 6 + src/lang/ta.rs | 6 + src/lang/template.rs | 6 + src/lang/th.rs | 6 + src/lang/tr.rs | 6 + src/lang/tw.rs | 6 + src/lang/uk.rs | 6 + src/lang/vi.rs | 6 + src/lib.rs | 3 +- src/platform/linux.rs | 51 + src/platform/macos.rs | 104 ++ src/platform/mod.rs | 15 +- src/platform/windows.rs | 47 +- src/server/connection.rs | 13 +- src/server/input_service.rs | 89 +- src/ui_session_interface.rs | 14 +- 90 files changed, 3089 insertions(+), 165 deletions(-) create mode 100644 flutter/lib/models/relative_mouse_model.dart create mode 100644 flutter/lib/utils/relative_mouse_accumulator.dart diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index df5b68eb4..d2828b819 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -39,7 +39,7 @@ env: # 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`. VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b" ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" # 2025.01.13, got "/opt/artifacts/vcpkg/vcpkg: No such file or directory" with latest version - VERSION: "1.4.4" + VERSION: "1.4.5" NDK_VERSION: "r27c" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" diff --git a/.github/workflows/playground.yml b/.github/workflows/playground.yml index 377b47ed4..0c7b450a3 100644 --- a/.github/workflows/playground.yml +++ b/.github/workflows/playground.yml @@ -17,7 +17,7 @@ env: TAG_NAME: "nightly" VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b" - VERSION: "1.4.4" + VERSION: "1.4.5" NDK_VERSION: "r26d" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml index 6fa17c9da..ce54723e9 100644 --- a/.github/workflows/winget.yml +++ b/.github/workflows/winget.yml @@ -10,6 +10,6 @@ jobs: - uses: vedantmgoyal9/winget-releaser@main with: identifier: RustDesk.RustDesk - version: "1.4.4" - release-tag: "1.4.4" + version: "1.4.5" + release-tag: "1.4.5" token: ${{ secrets.WINGET_TOKEN }} diff --git a/Cargo.lock b/Cargo.lock index e3e40ec06..2c8cf996d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7134,7 +7134,7 @@ dependencies = [ [[package]] name = "rustdesk" -version = "1.4.4" +version = "1.4.5" dependencies = [ "android-wakelock", "android_logger", @@ -7249,7 +7249,7 @@ dependencies = [ [[package]] name = "rustdesk-portable-packer" -version = "1.4.4" +version = "1.4.5" dependencies = [ "brotli", "dirs 5.0.1", diff --git a/Cargo.toml b/Cargo.toml index 71894b660..890da5647 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk" -version = "1.4.4" +version = "1.4.5" authors = ["rustdesk "] edition = "2021" build= "build.rs" diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index d4409a1bb..d4af2d13a 100644 --- a/appimage/AppImageBuilder-aarch64.yml +++ b/appimage/AppImageBuilder-aarch64.yml @@ -18,7 +18,7 @@ AppDir: id: rustdesk name: rustdesk icon: rustdesk - version: 1.4.4 + version: 1.4.5 exec: usr/share/rustdesk/rustdesk exec_args: $@ apt: diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index 767bf6bc0..d85bd381e 100644 --- a/appimage/AppImageBuilder-x86_64.yml +++ b/appimage/AppImageBuilder-x86_64.yml @@ -18,7 +18,7 @@ AppDir: id: rustdesk name: rustdesk icon: rustdesk - version: 1.4.4 + version: 1.4.5 exec: usr/share/rustdesk/rustdesk exec_args: $@ apt: diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index bd7948de0..eca7fa05a 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1011,13 +1011,15 @@ makeMobileActionsOverlayEntry(VoidCallback? onHide, {FFI? ffi}) { }); } -void showToast(String text, {Duration timeout = const Duration(seconds: 3)}) { +void showToast(String text, + {Duration timeout = const Duration(seconds: 3), + Alignment alignment = const Alignment(0.0, 0.8)}) { final overlayState = globalKey.currentState?.overlay; if (overlayState == null) return; final entry = OverlayEntry(builder: (context) { return IgnorePointer( child: Align( - alignment: const Alignment(0.0, 0.8), + alignment: alignment, child: Container( decoration: BoxDecoration( color: MyTheme.color(context).toastBg, @@ -4069,3 +4071,23 @@ String decode_http_response(http.Response resp) { bool peerTabShowNote(PeerTabIndex peerTabIndex) { return peerTabIndex == PeerTabIndex.ab || peerTabIndex == PeerTabIndex.group; } + +// TODO: We should support individual bits combinations in the future. +// But for now, just keep it simple, because the old code only supports single button. +// No users have requested multi-button support yet. +String mouseButtonsToPeer(int buttons) { + switch (buttons) { + case kPrimaryMouseButton: + return 'left'; + case kSecondaryMouseButton: + return 'right'; + case kMiddleMouseButton: + return 'wheel'; + case kBackMouseButton: + return 'back'; + case kForwardMouseButton: + return 'forward'; + default: + return ''; + } +} diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index f75e0027b..95a716042 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -372,7 +372,10 @@ class _RawTouchGestureDetectorRegionState await ffi.cursorModel .move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy); } - await inputModel.sendMouse('down', MouseButtons.left); + // In relative mouse mode, skip mouse down - only send movement via sendMobileRelativeMouseMove + if (!inputModel.relativeMouseMode.value) { + await inputModel.sendMouse('down', MouseButtons.left); + } await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); } else { final offset = ffi.cursorModel.offset; @@ -397,7 +400,12 @@ class _RawTouchGestureDetectorRegionState if (handleTouch && !_touchModePanStarted) { return; } - await ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch); + // In relative mouse mode, send delta directly without position tracking. + if (inputModel.relativeMouseMode.value) { + await inputModel.sendMobileRelativeMouseMove(d.delta.dx, d.delta.dy); + } else { + await ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch); + } } onOneFingerPanEnd(DragEndDetails d) async { @@ -409,7 +417,10 @@ class _RawTouchGestureDetectorRegionState ffi.cursorModel.clearRemoteWindowCoords(); } if (handleTouch) { - await inputModel.sendMouse('up', MouseButtons.left); + // In relative mouse mode, skip mouse up - matches the skipped mouse down in onOneFingerPanStart + if (!inputModel.relativeMouseMode.value) { + await inputModel.sendMouse('up', MouseButtons.left); + } } } diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index 929acbfcf..a46ce54fd 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -831,6 +831,7 @@ List toolbarKeyboardToggles(FFI ffi) { final ffiModel = ffi.ffiModel; final pi = ffiModel.pi; final sessionId = ffi.sessionId; + final isDefaultConn = ffi.connType == ConnType.defaultConn; List v = []; // swap key @@ -852,6 +853,34 @@ List toolbarKeyboardToggles(FFI ffi) { child: Text(translate('Swap control-command key')))); } + // Relative mouse mode (gaming mode). + // Only show when server supports MOUSE_TYPE_MOVE_RELATIVE (version >= 1.4.5) + // Note: This feature is only available in Flutter client. Sciter client does not support this. + // Web client is not supported yet due to Pointer Lock API integration complexity with Flutter's input system. + // Wayland is not supported due to cursor warping limitations. + // Mobile: This option is now in GestureHelp widget, shown only when joystick is visible. + final isWayland = isDesktop && isLinux && bind.mainCurrentIsWayland(); + if (isDesktop && + isDefaultConn && + !isWeb && + !isWayland && + ffiModel.keyboard && + !ffiModel.viewOnly && + ffi.inputModel.isRelativeMouseModeSupported) { + v.add(TToggleMenu( + value: ffi.inputModel.relativeMouseMode.value, + onChanged: (value) { + if (value == null) return; + final previousValue = ffi.inputModel.relativeMouseMode.value; + final success = ffi.inputModel.setRelativeMouseMode(value); + if (!success) { + // Revert the observable toggle to reflect the actual state + ffi.inputModel.relativeMouseMode.value = previousValue; + } + }, + child: Text(translate('Relative mouse mode')))); + } + // reverse mouse wheel if (ffiModel.keyboard) { var optionValue = diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index aea744a78..78b1f261a 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -258,6 +258,33 @@ const int kMinTrackpadSpeed = 10; const int kDefaultTrackpadSpeed = 100; const int kMaxTrackpadSpeed = 1000; +// relative mouse mode +/// Throttle duration (in milliseconds) for updating pointer lock center during +/// window move/resize events. Lower values provide more responsive updates but +/// may cause performance issues during rapid window operations. +const int kDefaultPointerLockCenterThrottleMs = 100; + +/// Minimum server version required for relative mouse mode (MOUSE_TYPE_MOVE_RELATIVE). +/// Servers older than this version will ignore relative mouse events. +/// +/// IMPORTANT: This value must be kept in sync with the Rust constant +/// `MIN_VERSION_RELATIVE_MOUSE_MODE` in `src/common.rs`. +const String kMinVersionForRelativeMouseMode = '1.4.5'; + +/// Maximum delta value for relative mouse movement. +/// Large values could cause issues with i32 overflow on server side, +/// and no reasonable mouse movement should exceed this bound. +/// +/// IMPORTANT: This value must be kept in sync with the Rust constant +/// `MAX_RELATIVE_MOUSE_DELTA` in `src/server/input_service.rs`. +const int kMaxRelativeMouseDelta = 10000; + +/// Debounce duration (in milliseconds) for relative mouse mode toggle. +/// This prevents double-toggle from race condition between Rust rdev grab loop +/// and Flutter keyboard handling. Value should be small enough to allow +/// intentional quick toggles but large enough to prevent accidental double-triggers. +const int kRelativeMouseModeToggleDebounceMs = 150; + // incomming (should be incoming) is kept, because change it will break the previous setting. const String kKeyPrinterIncomingJobAction = 'printer-incomming-job-action'; const String kValuePrinterIncomingJobDismiss = 'dismiss'; diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 3c5245bb3..29e710bbc 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -15,6 +15,7 @@ import '../../common.dart'; import '../../common/widgets/dialog.dart'; import '../../common/widgets/toolbar.dart'; import '../../models/model.dart'; +import '../../models/input_model.dart'; import '../../models/platform_model.dart'; import '../../common/shared_state.dart'; import '../../utils/image.dart'; @@ -90,6 +91,10 @@ class _RemotePageState extends State final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode"); + // Debounce timer for pointer lock center updates during window events. + // Uses kDefaultPointerLockCenterThrottleMs from consts.dart for the duration. + Timer? _pointerLockCenterDebounceTimer; + // We need `_instanceIdOnEnterOrLeaveImage4Toolbar` together with `_onEnterOrLeaveImage4Toolbar` // to identify the toolbar instance and its callback function. int? _instanceIdOnEnterOrLeaveImage4Toolbar; @@ -169,6 +174,16 @@ class _RemotePageState extends State WidgetsBinding.instance.addPostFrameCallback((_) { widget.tabController?.onSelected?.call(widget.id); }); + + // Register callback to cancel debounce timer when relative mouse mode is disabled + _ffi.inputModel.onRelativeMouseModeDisabled = + _cancelPointerLockCenterDebounceTimer; + } + + /// Cancel the pointer lock center debounce timer + void _cancelPointerLockCenterDebounceTimer() { + _pointerLockCenterDebounceTimer?.cancel(); + _pointerLockCenterDebounceTimer = null; } @override @@ -184,6 +199,13 @@ class _RemotePageState extends State _rawKeyFocusNode.unfocus(); } stateGlobal.isFocused.value = false; + + // When window loses focus, temporarily release relative mouse mode constraints + // to allow user to interact with other applications normally. + // The cursor will be re-hidden and re-centered when window regains focus. + if (_ffi.inputModel.relativeMouseMode.value) { + _ffi.inputModel.onWindowBlur(); + } } @override @@ -194,6 +216,12 @@ class _RemotePageState extends State _isWindowBlur = false; } stateGlobal.isFocused.value = true; + + // Restore relative mouse mode constraints when window regains focus. + if (_ffi.inputModel.relativeMouseMode.value) { + _rawKeyFocusNode.requestFocus(); + _ffi.inputModel.onWindowFocus(); + } } @override @@ -205,6 +233,8 @@ class _RemotePageState extends State _isWindowBlur = false; } WakelockManager.enable(_uniqueKey); + // Update pointer lock center when window is restored + _updatePointerLockCenterIfNeeded(); } // When the window is unminimized, onWindowMaximize or onWindowRestore can be called when the old state was maximized or not. @@ -212,12 +242,50 @@ class _RemotePageState extends State void onWindowMaximize() { super.onWindowMaximize(); WakelockManager.enable(_uniqueKey); + // Update pointer lock center when window is maximized + _updatePointerLockCenterIfNeeded(); + } + + @override + void onWindowResize() { + super.onWindowResize(); + // Update pointer lock center when window is resized + _updatePointerLockCenterIfNeeded(); + } + + @override + void onWindowMove() { + super.onWindowMove(); + // Update pointer lock center when window is moved + _updatePointerLockCenterIfNeeded(); + } + + /// Update pointer lock center with debouncing to avoid excessive updates + /// during rapid window move/resize events. + void _updatePointerLockCenterIfNeeded() { + if (!_ffi.inputModel.relativeMouseMode.value) return; + + // Cancel any pending update and schedule a new one (debounce pattern) + _pointerLockCenterDebounceTimer?.cancel(); + _pointerLockCenterDebounceTimer = Timer( + const Duration(milliseconds: kDefaultPointerLockCenterThrottleMs), + () { + if (!mounted) return; + if (_ffi.inputModel.relativeMouseMode.value) { + _ffi.inputModel.updatePointerLockCenter(); + } + }, + ); } @override void onWindowMinimize() { super.onWindowMinimize(); WakelockManager.disable(_uniqueKey); + // Release cursor constraints when minimized + if (_ffi.inputModel.relativeMouseMode.value) { + _ffi.inputModel.onWindowBlur(); + } } @override @@ -243,6 +311,16 @@ class _RemotePageState extends State // https://github.com/flutter/flutter/issues/64935 super.dispose(); debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}"); + + // Defensive cleanup: ensure host system-key propagation is reset even if + // MouseRegion.onExit never fired (e.g., tab closed while cursor inside). + if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true); + + _pointerLockCenterDebounceTimer?.cancel(); + _pointerLockCenterDebounceTimer = null; + // Clear callback reference to prevent memory leaks and stale references + _ffi.inputModel.onRelativeMouseModeDisabled = null; + // Relative mouse mode cleanup is centralized in FFI.close(closeSession: ...). _ffi.textureModel.onRemotePageDispose(closeSession); if (closeSession) { // ensure we leave this session, this is a double check @@ -344,10 +422,15 @@ class _RemotePageState extends State } }(), // Use Overlay to enable rebuild every time on menu button click. - _ffi.ffiModel.pi.isSet.isTrue - ? Overlay( - initialEntries: [OverlayEntry(builder: remoteToolbar)]) - : remoteToolbar(context), + // Hide toolbar when relative mouse mode is active to prevent + // cursor from escaping to toolbar area. + Obx(() => _ffi.inputModel.relativeMouseMode.value + ? const Offstage() + : _ffi.ffiModel.pi.isSet.isTrue + ? Overlay(initialEntries: [ + OverlayEntry(builder: remoteToolbar) + ]) + : remoteToolbar(context)), _ffi.ffiModel.pi.isSet.isFalse ? emptyOverlay() : Offstage(), ], ), @@ -415,6 +498,7 @@ class _RemotePageState extends State // } } + // See [onWindowBlur]. if (!isWindows) { if (!_rawKeyFocusNode.hasFocus) { @@ -440,6 +524,7 @@ class _RemotePageState extends State // } } + // See [onWindowBlur]. if (!isWindows) { _ffi.inputModel.enterOrLeave(false); @@ -487,32 +572,39 @@ class _RemotePageState extends State Widget getBodyForDesktop(BuildContext context) { var paints = [ - MouseRegion(onEnter: (evt) { - if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: false); - }, onExit: (evt) { - if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true); - }, child: LayoutBuilder(builder: (context, constraints) { - final c = Provider.of(context, listen: false); - Future.delayed(Duration.zero, () => c.updateViewStyle()); - final peerDisplay = CurrentDisplayState.find(widget.id); - return Obx( - () => _ffi.ffiModel.pi.isSet.isFalse - ? Container(color: Colors.transparent) - : Obx(() { - _ffi.textureModel.updateCurrentDisplay(peerDisplay.value); - return ImagePaint( - id: widget.id, - zoomCursor: _zoomCursor, - cursorOverImage: _cursorOverImage, - keyboardEnabled: _keyboardEnabled, - remoteCursorMoved: _remoteCursorMoved, - listenerBuilder: (child) => _buildRawTouchAndPointerRegion( - child, enterView, leaveView), - ffi: _ffi, - ); - }), - ); - })) + MouseRegion( + onEnter: (evt) { + if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: false); + }, + onExit: (evt) { + if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true); + }, + child: _ViewStyleUpdater( + canvasModel: _ffi.canvasModel, + inputModel: _ffi.inputModel, + child: Builder(builder: (context) { + final peerDisplay = CurrentDisplayState.find(widget.id); + return Obx( + () => _ffi.ffiModel.pi.isSet.isFalse + ? Container(color: Colors.transparent) + : Obx(() { + _ffi.textureModel.updateCurrentDisplay(peerDisplay.value); + return ImagePaint( + id: widget.id, + zoomCursor: _zoomCursor, + cursorOverImage: _cursorOverImage, + keyboardEnabled: _keyboardEnabled, + remoteCursorMoved: _remoteCursorMoved, + listenerBuilder: (child) => + _buildRawTouchAndPointerRegion( + child, enterView, leaveView), + ffi: _ffi, + ); + }), + ); + }), + ), + ) ]; if (!_ffi.canvasModel.cursorEmbedded) { @@ -541,6 +633,63 @@ class _RemotePageState extends State bool get wantKeepAlive => true; } +/// A widget that tracks the view size and updates CanvasModel.updateViewStyle() +/// and InputModel.updateImageWidgetSize() only when size actually changes. +/// This avoids scheduling post-frame callbacks on every LayoutBuilder rebuild. +class _ViewStyleUpdater extends StatefulWidget { + final CanvasModel canvasModel; + final InputModel inputModel; + final Widget child; + + const _ViewStyleUpdater({ + Key? key, + required this.canvasModel, + required this.inputModel, + required this.child, + }) : super(key: key); + + @override + State<_ViewStyleUpdater> createState() => _ViewStyleUpdaterState(); +} + +class _ViewStyleUpdaterState extends State<_ViewStyleUpdater> { + Size? _lastSize; + bool _callbackScheduled = false; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final maxWidth = constraints.maxWidth; + final maxHeight = constraints.maxHeight; + // Guard against infinite constraints (e.g., unconstrained ancestor). + if (!maxWidth.isFinite || !maxHeight.isFinite) { + return widget.child; + } + final newSize = Size(maxWidth, maxHeight); + if (_lastSize != newSize) { + _lastSize = newSize; + // Schedule the update for after the current frame to avoid setState during build. + // Use _callbackScheduled flag to prevent accumulating multiple callbacks + // when size changes rapidly before any callback executes. + if (!_callbackScheduled) { + _callbackScheduled = true; + SchedulerBinding.instance.addPostFrameCallback((_) { + _callbackScheduled = false; + final currentSize = _lastSize; + if (mounted && currentSize != null) { + widget.canvasModel.updateViewStyle(); + widget.inputModel.updateImageWidgetSize(currentSize); + } + }); + } + } + return widget.child; + }, + ); + } +} + class ImagePaint extends StatefulWidget { final FFI ffi; final String id; @@ -605,21 +754,24 @@ class _ImagePaintState extends State { cursor: cursorOverImage.isTrue ? c.cursorEmbedded ? SystemMouseCursors.none - : keyboardEnabled.isTrue - ? (() { - if (remoteCursorMoved.isTrue) { - _lastRemoteCursorMoved = true; - return SystemMouseCursors.none; - } else { - if (_lastRemoteCursorMoved) { - _lastRemoteCursorMoved = false; - _firstEnterImage.value = true; - } - return _buildCustomCursor( - context, getCursorScale()); - } - }()) - : _buildDisabledCursor(context, getCursorScale()) + // Hide cursor when relative mouse mode is active + : widget.ffi.inputModel.relativeMouseMode.value + ? SystemMouseCursors.none + : keyboardEnabled.isTrue + ? (() { + if (remoteCursorMoved.isTrue) { + _lastRemoteCursorMoved = true; + return SystemMouseCursors.none; + } else { + if (_lastRemoteCursorMoved) { + _lastRemoteCursorMoved = false; + _firstEnterImage.value = true; + } + return _buildCustomCursor( + context, getCursorScale()); + } + }()) + : _buildDisabledCursor(context, getCursorScale()) : MouseCursor.defer, onHover: (evt) {}, child: child); diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index af285ac35..ccd5935ce 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -135,7 +135,13 @@ class _ConnectionTabPageState extends State { body: DesktopTab( controller: tabController, onWindowCloseButton: handleWindowCloseButton, - tail: const AddButton(), + tail: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _RelativeMouseModeHint(tabController: tabController), + const AddButton(), + ], + ), selectedBorderColor: MyTheme.accent, pageViewBuilder: (pageView) => pageView, labelGetter: DesktopTab.tablabelGetter, @@ -374,6 +380,8 @@ class _ConnectionTabPageState extends State { loopCloseWindow(); } ConnectionTypeState.delete(id); + // Clean up relative mouse mode state for this peer. + stateGlobal.relativeMouseModeState.remove(id); _update_remote_count(); } @@ -548,3 +556,69 @@ class _ConnectionTabPageState extends State { return returnValue; } } + +/// A widget that displays a hint in the tab bar when relative mouse mode is active. +/// This helps users remember how to exit relative mouse mode. +class _RelativeMouseModeHint extends StatelessWidget { + final DesktopTabController tabController; + + const _RelativeMouseModeHint({Key? key, required this.tabController}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Obx(() { + // Check if there are any tabs + if (tabController.state.value.tabs.isEmpty) { + return const SizedBox.shrink(); + } + + // Get current selected tab's RemotePage + final selectedTabInfo = tabController.state.value.selectedTabInfo; + if (selectedTabInfo.page is! RemotePage) { + return const SizedBox.shrink(); + } + + final remotePage = selectedTabInfo.page as RemotePage; + final String peerId = remotePage.id; + + // Use global state to check relative mouse mode (synced from InputModel). + // This avoids timing issues with FFI registration. + final isRelativeMouseMode = + stateGlobal.relativeMouseModeState[peerId] ?? false; + + if (!isRelativeMouseMode) { + return const SizedBox.shrink(); + } + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + margin: const EdgeInsets.only(right: 8), + decoration: BoxDecoration( + color: Colors.orange.withOpacity(0.2), + borderRadius: BorderRadius.circular(4), + border: Border.all(color: Colors.orange.withOpacity(0.5)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.mouse, + size: 14, + color: Colors.orange[700], + ), + const SizedBox(width: 4), + Text( + translate( + 'rel-mouse-exit-{${isMacOS ? "Cmd+G" : "Ctrl+Alt"}}-tip'), + style: TextStyle( + fontSize: 11, + color: Colors.orange[700], + ), + ), + ], + ), + ); + }); + } +} diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index cf601557a..ac7d80017 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -593,7 +593,6 @@ class _DesktopTabState extends State Widget _buildBar() { return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: GestureDetector( diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index dd783055a..22dbebce6 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -569,7 +569,9 @@ class _RemotePageState extends State with WidgetsBindingObserver { } bool get showCursorPaint => - !gFFI.ffiModel.isPeerAndroid && !gFFI.canvasModel.cursorEmbedded; + !gFFI.ffiModel.isPeerAndroid && + !gFFI.canvasModel.cursorEmbedded && + !gFFI.inputModel.relativeMouseMode.value; Widget getBodyForMobile() { final keyboardIsVisible = keyboardVisibilityController.isVisible; @@ -808,6 +810,7 @@ class _RemotePageState extends State with WidgetsBindingObserver { bind.mainSetLocalOption(key: kOptionTouchMode, value: v); }, virtualMouseMode: gFFI.ffiModel.virtualMouseMode, + inputModel: gFFI.inputModel, ))); } diff --git a/flutter/lib/mobile/widgets/floating_mouse_widgets.dart b/flutter/lib/mobile/widgets/floating_mouse_widgets.dart index ddb20860c..dbcc606af 100644 --- a/flutter/lib/mobile/widgets/floating_mouse_widgets.dart +++ b/flutter/lib/mobile/widgets/floating_mouse_widgets.dart @@ -83,7 +83,10 @@ class _FloatingMouseWidgetsState extends State { cursorModel: _cursorModel, ), if (virtualMouseMode.showVirtualJoystick) - VirtualJoystick(cursorModel: _cursorModel), + VirtualJoystick( + cursorModel: _cursorModel, + inputModel: _inputModel, + ), FloatingLeftRightButton( isLeft: true, inputModel: _inputModel, @@ -674,12 +677,18 @@ class _QuarterCirclePainter extends CustomPainter { bool shouldRepaint(CustomPainter oldDelegate) => false; } -// Virtual joystick sends the absolute movement for now. -// Maybe we need to change it to relative movement in the future. +// Virtual joystick can send either absolute movement (via updatePan) +// or relative movement (via sendMobileRelativeMouseMove) depending on the +// InputModel.relativeMouseMode setting. class VirtualJoystick extends StatefulWidget { final CursorModel cursorModel; + final InputModel inputModel; - const VirtualJoystick({super.key, required this.cursorModel}); + const VirtualJoystick({ + super.key, + required this.cursorModel, + required this.inputModel, + }); @override State createState() => _VirtualJoystickState(); @@ -694,6 +703,10 @@ class _VirtualJoystickState extends State { final double _moveStep = 3.0; final double _speed = 1.0; + /// Scale factor for relative mouse movement sensitivity. + /// Higher values result in faster cursor movement on the remote machine. + static const double _kRelativeMouseScale = 3.0; + // One-shot timer to detect a drag gesture Timer? _dragStartTimer; // Periodic timer for continuous movement @@ -701,6 +714,9 @@ class _VirtualJoystickState extends State { Size? _lastScreenSize; bool _isPressed = false; + /// Check if relative mouse mode is enabled. + bool get _useRelativeMouse => widget.inputModel.relativeMouseMode.value; + @override void initState() { super.initState(); @@ -746,6 +762,18 @@ class _VirtualJoystickState extends State { ); } + /// Send movement delta to remote machine. + /// Uses relative mouse mode if enabled, otherwise uses absolute updatePan. + void _sendMovement(Offset delta) { + if (_useRelativeMouse) { + widget.inputModel.sendMobileRelativeMouseMove( + delta.dx * _kRelativeMouseScale, delta.dy * _kRelativeMouseScale); + } else { + // In absolute mode, use cursorModel.updatePan which tracks position. + widget.cursorModel.updatePan(delta, Offset.zero, false); + } + } + void _stopSendEventTimer() { _dragStartTimer?.cancel(); _continuousMoveTimer?.cancel(); @@ -773,7 +801,7 @@ class _VirtualJoystickState extends State { // The movement is small for a gentle start. final initialDelta = _offsetToPanDelta(_offset); if (initialDelta.distance > 0) { - widget.cursorModel.updatePan(initialDelta, Offset.zero, false); + _sendMovement(initialDelta); } // 2. Start a one-shot timer to check if the user is holding for a drag. @@ -784,10 +812,7 @@ class _VirtualJoystickState extends State { _continuousMoveTimer = periodic_immediate(const Duration(milliseconds: 20), () async { if (_offset != Offset.zero) { - widget.cursorModel.updatePan( - _offsetToPanDelta(_offset) * _moveStep * _speed, - Offset.zero, - false); + _sendMovement(_offsetToPanDelta(_offset) * _moveStep * _speed); } }); }); diff --git a/flutter/lib/mobile/widgets/gesture_help.dart b/flutter/lib/mobile/widgets/gesture_help.dart index 30150be5a..8e86681b4 100644 --- a/flutter/lib/mobile/widgets/gesture_help.dart +++ b/flutter/lib/mobile/widgets/gesture_help.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/models/input_model.dart'; import 'package:flutter_hbb/models/model.dart'; +import 'package:get/get.dart'; import 'package:toggle_switch/toggle_switch.dart'; class GestureIcons { @@ -39,11 +41,13 @@ class GestureHelp extends StatefulWidget { {Key? key, required this.touchMode, required this.onTouchModeChange, - required this.virtualMouseMode}) + required this.virtualMouseMode, + this.inputModel}) : super(key: key); final bool touchMode; final OnTouchModeChange onTouchModeChange; final VirtualMouseMode virtualMouseMode; + final InputModel? inputModel; @override State createState() => @@ -61,6 +65,14 @@ class _GestureHelpState extends State { _selectedIndex = _touchMode ? 1 : 0; } + /// Helper to exit relative mouse mode when certain conditions are met. + /// This reduces code duplication across multiple UI callbacks. + void _exitRelativeMouseModeIf(bool condition) { + if (condition) { + widget.inputModel?.setRelativeMouseMode(false); + } + } + @override Widget build(BuildContext context) { final size = MediaQuery.of(context).size; @@ -103,6 +115,8 @@ class _GestureHelpState extends State { _selectedIndex = index ?? 0; _touchMode = index == 0 ? false : true; widget.onTouchModeChange(_touchMode); + // Exit relative mouse mode when switching to touch mode + _exitRelativeMouseModeIf(_touchMode); } }); }, @@ -117,12 +131,18 @@ class _GestureHelpState extends State { onChanged: (value) async { if (value == null) return; await _virtualMouseMode.toggleVirtualMouse(); + // Exit relative mouse mode when virtual mouse is hidden + _exitRelativeMouseModeIf( + !_virtualMouseMode.showVirtualMouse); setState(() {}); }, ), InkWell( onTap: () async { await _virtualMouseMode.toggleVirtualMouse(); + // Exit relative mouse mode when virtual mouse is hidden + _exitRelativeMouseModeIf( + !_virtualMouseMode.showVirtualMouse); setState(() {}); }, child: Text(translate('Show virtual mouse')), @@ -196,6 +216,10 @@ class _GestureHelpState extends State { if (value == null) return; await _virtualMouseMode .toggleVirtualJoystick(); + // Exit relative mouse mode when joystick is hidden + _exitRelativeMouseModeIf( + !_virtualMouseMode + .showVirtualJoystick); setState(() {}); }, ), @@ -203,6 +227,10 @@ class _GestureHelpState extends State { onTap: () async { await _virtualMouseMode .toggleVirtualJoystick(); + // Exit relative mouse mode when joystick is hidden + _exitRelativeMouseModeIf( + !_virtualMouseMode + .showVirtualJoystick); setState(() {}); }, child: Text( @@ -211,6 +239,39 @@ class _GestureHelpState extends State { ], )), ), + // Relative mouse mode option - only visible when joystick is shown + if (!_touchMode && + _virtualMouseMode.showVirtualMouse && + _virtualMouseMode.showVirtualJoystick && + widget.inputModel != null) + Obx(() => Transform.translate( + offset: const Offset(-10.0, -24.0), + child: Padding( + // Indent further for 'Relative mouse mode' + padding: const EdgeInsets.only(left: 48.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Checkbox( + value: widget.inputModel! + .relativeMouseMode.value, + onChanged: (value) { + if (value == null) return; + widget.inputModel! + .setRelativeMouseMode(value); + }, + ), + InkWell( + onTap: () { + widget.inputModel! + .toggleRelativeMouseMode(); + }, + child: Text( + translate('Relative mouse mode')), + ), + ], + )), + )), ], ), ), diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 29d0cc0fd..c14a23739 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -14,6 +14,8 @@ import 'package:get/get.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; +import '../../models/state_model.dart'; +import 'relative_mouse_model.dart'; import '../common.dart'; import '../consts.dart'; @@ -349,15 +351,28 @@ class InputModel { double _trackpadSpeedInner = kDefaultTrackpadSpeed / 100.0; var _trackpadScrollUnsent = Offset.zero; + // Mobile relative mouse delta accumulators (for slow/fine movements). + double _mobileDeltaRemainderX = 0.0; + double _mobileDeltaRemainderY = 0.0; + var _lastScale = 1.0; bool _pointerMovedAfterEnter = false; + bool _pointerInsideImage = false; // mouse final isPhysicalMouse = false.obs; int _lastButtons = 0; Offset lastMousePos = Offset.zero; + // Relative mouse mode (for games/3D apps). + final relativeMouseMode = false.obs; + late final RelativeMouseModel _relativeMouse; + // Callback to cancel external throttle timer when relative mouse mode is disabled. + VoidCallback? onRelativeMouseModeDisabled; + // Disposer for the relativeMouseMode observer (to prevent memory leaks). + Worker? _relativeMouseModeDisposer; + bool _queryOtherWindowCoords = false; Rect? _windowRect; List _remoteWindowCoords = []; @@ -367,15 +382,40 @@ class InputModel { bool get keyboardPerm => parent.target!.ffiModel.keyboard; String get id => parent.target?.id ?? ''; String? get peerPlatform => parent.target?.ffiModel.pi.platform; + String get peerVersion => parent.target?.ffiModel.pi.version ?? ''; bool get isViewOnly => parent.target!.ffiModel.viewOnly; bool get showMyCursor => parent.target!.ffiModel.showMyCursor; double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio; bool get isViewCamera => parent.target!.connType == ConnType.viewCamera; int get trackpadSpeed => _trackpadSpeed; - bool get useEdgeScroll => parent.target!.canvasModel.scrollStyle == ScrollStyle.scrolledge; + bool get useEdgeScroll => + parent.target!.canvasModel.scrollStyle == ScrollStyle.scrolledge; + + /// Check if the connected server supports relative mouse mode. + bool get isRelativeMouseModeSupported => _relativeMouse.isSupported; InputModel(this.parent) { sessionId = parent.target!.sessionId; + _relativeMouse = RelativeMouseModel( + sessionId: sessionId, + enabled: relativeMouseMode, + keyboardPerm: () => keyboardPerm, + isViewCamera: () => isViewCamera, + peerVersion: () => peerVersion, + peerPlatform: () => peerPlatform, + modify: (msg) => modify(msg), + getPointerInsideImage: () => _pointerInsideImage, + setPointerInsideImage: (inside) => _pointerInsideImage = inside, + ); + _relativeMouse.onDisabled = () => onRelativeMouseModeDisabled?.call(); + + // Sync relative mouse mode state to global state for UI components (e.g., tab bar hint). + _relativeMouseModeDisposer = ever(relativeMouseMode, (bool value) { + final peerId = id; + if (peerId.isNotEmpty) { + stateGlobal.relativeMouseModeState[peerId] = value; + } + }); } // This function must be called after the peer info is received. @@ -506,6 +546,10 @@ class InputModel { } } + if (_relativeMouse.handleRawKeyEvent(e)) { + return KeyEventResult.handled; + } + final key = e.logicalKey; if (e is RawKeyDownEvent) { if (!e.repeat) { @@ -568,6 +612,16 @@ class InputModel { } } + if (_relativeMouse.handleKeyEvent( + e, + ctrlPressed: ctrl, + shiftPressed: shift, + altPressed: alt, + commandPressed: command, + )) { + return KeyEventResult.handled; + } + if (e is KeyUpEvent) { handleKeyUpEventModifiers(e); } else if (e is KeyDownEvent) { @@ -853,11 +907,13 @@ class InputModel { toReleaseKeys.release(handleKeyEvent); toReleaseRawKeys.release(handleRawKeyEvent); _pointerMovedAfterEnter = false; + _pointerInsideImage = enter; // Fix status if (!enter) { resetModifiers(); } + _relativeMouse.onEnterOrLeaveImage(enter); _flingTimer?.cancel(); if (!isInputSourceFlutter) { bind.sessionEnterOrLeave(sessionId: sessionId, enter: enter); @@ -878,15 +934,134 @@ class InputModel { msg: json.encode(modify({'x': '$x2', 'y': '$y2'}))); } + /// Send relative mouse movement for mobile clients (virtual joystick). + /// This method is for touch-based controls that want to send delta values. + /// Uses the 'move_relative' type which bypasses absolute position tracking. + /// + /// Accumulates fractional deltas to avoid losing slow/fine movements. + /// Only sends events when relative mouse mode is enabled and supported. + Future sendMobileRelativeMouseMove(double dx, double dy) async { + if (!keyboardPerm) return; + if (isViewCamera) return; + // Only send relative mouse events when relative mode is enabled and supported. + if (!isRelativeMouseModeSupported || !relativeMouseMode.value) return; + _mobileDeltaRemainderX += dx; + _mobileDeltaRemainderY += dy; + final x = _mobileDeltaRemainderX.truncate(); + final y = _mobileDeltaRemainderY.truncate(); + _mobileDeltaRemainderX -= x; + _mobileDeltaRemainderY -= y; + if (x == 0 && y == 0) return; + await bind.sessionSendMouse( + sessionId: sessionId, + msg: json.encode(modify({ + 'type': 'move_relative', + 'x': '$x', + 'y': '$y', + }))); + } + + /// Update the pointer lock center position based on current window frame. + Future updatePointerLockCenter({Offset? localCenter}) { + return _relativeMouse.updatePointerLockCenter(localCenter: localCenter); + } + + /// Get the current image widget size (for comparison to avoid unnecessary updates). + Size? get imageWidgetSize => _relativeMouse.imageWidgetSize; + + /// Update the image widget size for center calculation. + void updateImageWidgetSize(Size size) { + _relativeMouse.updateImageWidgetSize(size); + } + + void toggleRelativeMouseMode() { + _relativeMouse.toggleRelativeMouseMode(); + } + + bool setRelativeMouseMode(bool enabled) { + return _relativeMouse.setRelativeMouseMode(enabled); + } + + /// Exit relative mouse mode and release all modifier keys to the remote. + /// This is called when the user presses the exit shortcut (Ctrl+Alt on Win/Linux, Cmd+G on macOS). + /// We need to send key-up events for all modifiers because the shortcut itself may have + /// blocked some key events, leaving the remote in a state where modifiers are stuck. + void exitRelativeMouseModeWithKeyRelease() { + if (!_relativeMouse.enabled.value) return; + + // First, send release events for all modifier keys to the remote. + // This ensures the remote doesn't have stuck modifier keys after exiting. + // Use press: false, down: false to send key-up events without modifiers attached. + final modifiersToRelease = [ + 'Control_L', + 'Control_R', + 'Alt_L', + 'Alt_R', + 'Shift_L', + 'Shift_R', + 'Meta_L', // Command/Super left + 'Meta_R', // Command/Super right + ]; + + for (final key in modifiersToRelease) { + bind.sessionInputKey( + sessionId: sessionId, + name: key, + down: false, + press: false, + alt: false, + ctrl: false, + shift: false, + command: false, + ); + } + + // Reset local modifier state + resetModifiers(); + + // Now exit relative mouse mode + _relativeMouse.setRelativeMouseMode(false); + } + + void disposeRelativeMouseMode() { + _relativeMouse.dispose(); + onRelativeMouseModeDisabled = null; + // Cancel the relative mouse mode observer and clean up global state. + _relativeMouseModeDisposer?.dispose(); + _relativeMouseModeDisposer = null; + final peerId = id; + if (peerId.isNotEmpty) { + stateGlobal.relativeMouseModeState.remove(peerId); + } + } + + void onWindowBlur() { + _relativeMouse.onWindowBlur(); + } + + void onWindowFocus() { + _relativeMouse.onWindowFocus(); + } + void onPointHoverImage(PointerHoverEvent e) { _stopFling = true; if (isViewOnly && !showMyCursor) return; if (e.kind != ui.PointerDeviceKind.mouse) return; + + // Only update pointer region when relative mouse mode is enabled. + // This avoids unnecessary tracking when not in relative mode. + if (_relativeMouse.enabled.value) { + _relativeMouse.updatePointerRegionTopLeftGlobal(e); + } + if (!isPhysicalMouse.value) { isPhysicalMouse.value = true; } if (isPhysicalMouse.value) { - handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position, edgeScroll: useEdgeScroll); + if (!_relativeMouse.handleRelativeMouseMove(e.localPosition)) { + handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position, + edgeScroll: useEdgeScroll); + } } } @@ -1043,13 +1218,25 @@ class InputModel { _windowRect = null; if (isViewOnly && !showMyCursor) return; if (isViewCamera) return; + + if (_relativeMouse.enabled.value) { + _relativeMouse.updatePointerRegionTopLeftGlobal(e); + } + if (e.kind != ui.PointerDeviceKind.mouse) { if (isPhysicalMouse.value) { isPhysicalMouse.value = false; } } if (isPhysicalMouse.value) { - handleMouse(_getMouseEvent(e, _kMouseEventDown), e.position); + // In relative mouse mode, send button events without position. + // Use _relativeMouse.enabled.value consistently with the guard above. + if (_relativeMouse.enabled.value) { + _relativeMouse + .sendRelativeMouseButton(_getMouseEvent(e, _kMouseEventDown)); + } else { + handleMouse(_getMouseEvent(e, _kMouseEventDown), e.position); + } } } @@ -1057,9 +1244,21 @@ class InputModel { if (isDesktop) _queryOtherWindowCoords = false; if (isViewOnly && !showMyCursor) return; if (isViewCamera) return; + + if (_relativeMouse.enabled.value) { + _relativeMouse.updatePointerRegionTopLeftGlobal(e); + } + if (e.kind != ui.PointerDeviceKind.mouse) return; if (isPhysicalMouse.value) { - handleMouse(_getMouseEvent(e, _kMouseEventUp), e.position); + // In relative mouse mode, send button events without position. + // Use _relativeMouse.enabled.value consistently with the guard above. + if (_relativeMouse.enabled.value) { + _relativeMouse + .sendRelativeMouseButton(_getMouseEvent(e, _kMouseEventUp)); + } else { + handleMouse(_getMouseEvent(e, _kMouseEventUp), e.position); + } } } @@ -1067,6 +1266,11 @@ class InputModel { if (isViewOnly && !showMyCursor) return; if (isViewCamera) return; if (e.kind != ui.PointerDeviceKind.mouse) return; + + if (_relativeMouse.enabled.value) { + _relativeMouse.updatePointerRegionTopLeftGlobal(e); + } + if (_queryOtherWindowCoords) { Future.delayed(Duration.zero, () async { _windowRect = await fillRemoteCoordsAndGetCurFrame(_remoteWindowCoords); @@ -1074,7 +1278,10 @@ class InputModel { _queryOtherWindowCoords = false; } if (isPhysicalMouse.value) { - handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position, edgeScroll: useEdgeScroll); + if (!_relativeMouse.handleRelativeMouseMove(e.localPosition)) { + handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position, + edgeScroll: useEdgeScroll); + } } } @@ -1098,6 +1305,11 @@ class InputModel { return null; } + /// Handle scroll/wheel events. + /// Note: Scroll events intentionally use absolute positioning even in relative mouse mode. + /// This is because scroll events don't need relative positioning - they represent + /// scroll deltas that are independent of cursor position. Games and 3D applications + /// handle scroll events the same way regardless of mouse mode. void onPointerSignalImage(PointerSignalEvent e) { if (isViewOnly) return; if (isViewCamera) return; @@ -1285,14 +1497,18 @@ class InputModel { evt['y'] = '${pos.y.toInt()}'; } - Map mapButtons = { - kPrimaryMouseButton: 'left', - kSecondaryMouseButton: 'right', - kMiddleMouseButton: 'wheel', - kBackMouseButton: 'back', - kForwardMouseButton: 'forward' - }; - evt['buttons'] = mapButtons[evt['buttons']] ?? ''; + final buttons = evt['buttons']; + if (buttons is int) { + evt['buttons'] = mouseButtonsToPeer(buttons); + } else { + // Log warning if buttons exists but is not an int (unexpected caller). + // Keep empty string fallback for missing buttons to preserve move/hover behavior. + if (buttons != null) { + debugPrint( + '[InputModel] processEventToPeer: unexpected buttons type: ${buttons.runtimeType}, value: $buttons'); + } + evt['buttons'] = ''; + } return evt; } @@ -1303,8 +1519,8 @@ class InputModel { bool moveCanvas = true, bool edgeScroll = false, }) { - final evtToPeer = - processEventToPeer(evt, offset, onExit: onExit, moveCanvas: moveCanvas, edgeScroll: edgeScroll); + final evtToPeer = processEventToPeer(evt, offset, + onExit: onExit, moveCanvas: moveCanvas, edgeScroll: edgeScroll); if (evtToPeer != null) { bind.sessionSendMouse( sessionId: sessionId, msg: json.encode(modify(evtToPeer))); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index e2f509c13..578ba3ce3 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -213,6 +213,9 @@ class FfiModel with ChangeNotifier { } updatePermission(Map evt, String id) { + // Track previous keyboard permission to detect revocation. + final hadKeyboardPerm = _permissions['keyboard'] != false; + evt.forEach((k, v) { if (k == 'name' || k.isEmpty) return; _permissions[k] = v == 'true'; @@ -221,6 +224,18 @@ class FfiModel with ChangeNotifier { if (parent.target?.connType == ConnType.defaultConn) { KeyboardEnabledState.find(id).value = _permissions['keyboard'] != false; } + + // If keyboard permission was revoked while relative mouse mode is active, + // forcefully disable relative mouse mode to prevent the user from being trapped. + final hasKeyboardPerm = _permissions['keyboard'] != false; + if (hadKeyboardPerm && !hasKeyboardPerm) { + final inputModel = parent.target?.inputModel; + if (inputModel != null && inputModel.relativeMouseMode.value) { + inputModel.setRelativeMouseMode(false); + showToast(translate('rel-mouse-permission-lost-tip')); + } + } + debugPrint('updatePermission: $_permissions'); notifyListeners(); } @@ -457,6 +472,9 @@ class FfiModel with ChangeNotifier { _handlePrinterRequest(evt, sessionId, peerId); } else if (name == 'screenshot') { _handleScreenshot(evt, sessionId, peerId); + } else if (name == 'exit_relative_mouse_mode') { + // Handle exit shortcut from rdev grab loop (Ctrl+Alt on Win/Linux, Cmd+G on macOS) + parent.target?.inputModel.exitRelativeMouseModeWithKeyRelease(); } else { debugPrint('Event is not handled in the fixed branch: $name'); } @@ -765,7 +783,7 @@ class FfiModel with ChangeNotifier { } } - updateCurDisplay(SessionID sessionId, {updateCursorPos = false}) { + Future updateCurDisplay(SessionID sessionId, {updateCursorPos = false}) async { final newRect = displaysRect(); if (newRect == null) { return; @@ -777,9 +795,19 @@ class FfiModel with ChangeNotifier { updateCursorPos: updateCursorPos); } _rect = newRect; - parent.target?.canvasModel + // Await updateViewStyle to ensure view geometry is fully updated before + // updating pointer lock center. This prevents stale center calculations. + await parent.target?.canvasModel .updateViewStyle(refreshMousePos: updateCursorPos); _updateSessionWidthHeight(sessionId); + + // Keep pointer lock center in sync when using relative mouse mode. + // Note: updatePointerLockCenter is async-safe (handles errors internally), + // so we fire-and-forget here. + final inputModel = parent.target?.inputModel; + if (inputModel != null && inputModel.relativeMouseMode.value) { + inputModel.updatePointerLockCenter(); + } } } @@ -863,6 +891,17 @@ class FfiModel with ChangeNotifier { final title = evt['title']; final text = evt['text']; final link = evt['link']; + + // Disable relative mouse mode on any error-type message to ensure cursor is released. + // This includes connection errors, session-ending messages, elevation errors, etc. + // Safety: releasing pointer lock on errors prevents the user from being stuck. + if (title == 'Connection Error' || + type == 'error' || + type == 'restarting' || + (type is String && type.contains('error'))) { + parent.target?.inputModel.setRelativeMouseMode(false); + } + if (type == 're-input-password') { wrongPasswordDialog(sessionId, dialogManager, type, title, text); } else if (type == 'input-2fa') { @@ -967,6 +1006,8 @@ class FfiModel with ChangeNotifier { void reconnect(OverlayDialogManager dialogManager, SessionID sessionId, bool forceRelay) { + // Disable relative mouse mode before reconnecting to ensure cursor is released. + parent.target?.inputModel.setRelativeMouseMode(false); bind.sessionReconnect(sessionId: sessionId, forceRelay: forceRelay); clearPermissions(); dialogManager.dismissAll(); @@ -1192,9 +1233,6 @@ class FfiModel with ChangeNotifier { _queryAuditGuid(peerId); - // This call is to ensuer the keyboard mode is updated depending on the peer version. - parent.target?.inputModel.updateKeyboardMode(); - // Map clone is required here, otherwise "evt" may be changed by other threads through the reference. // Because this function is asynchronous, there's an "await" in this function. cachedPeerData.peerInfo = {...evt}; @@ -1206,6 +1244,17 @@ class FfiModel with ChangeNotifier { parent.target?.dialogManager.dismissAll(); _pi.version = evt['version']; + // Note: Relative mouse mode is NOT auto-enabled on connect. + // Users must manually enable it via toolbar or keyboard shortcut (Ctrl+Alt+Shift+M). + // + // For desktop/webDesktop, keyboard mode initialization is handled later by + // checkDesktopKeyboardMode() which may change the mode if not supported, + // followed by updateKeyboardMode() to sync InputModel.keyboardMode. + // For mobile, updateKeyboardMode() is currently a no-op (only executes on desktop/web), + // but we call it here for consistency and future-proofing. + if (isMobile) { + parent.target?.inputModel.updateKeyboardMode(); + } _pi.isSupportMultiUiSession = bind.isSupportMultiUiSession(version: _pi.version); _pi.username = evt['username']; @@ -1307,7 +1356,11 @@ class FfiModel with ChangeNotifier { stateGlobal.resetLastResolutionGroupValues(peerId); if (isDesktop || isWebDesktop) { - checkDesktopKeyboardMode(); + // checkDesktopKeyboardMode may change the keyboard mode if the current + // mode is not supported. Re-sync InputModel.keyboardMode afterwards. + // Note: updateKeyboardMode() is a no-op on mobile (early-returns). + await checkDesktopKeyboardMode(); + await parent.target?.inputModel.updateKeyboardMode(); } notifyListeners(); @@ -3768,6 +3821,8 @@ class FFI { ffiModel.clear(); canvasModel.clear(); inputModel.resetModifiers(); + // Dispose relative mouse mode resources to ensure cursor is restored + inputModel.disposeRelativeMouseMode(); if (closeSession) { await bind.sessionClose(sessionId: sessionId); } diff --git a/flutter/lib/models/relative_mouse_model.dart b/flutter/lib/models/relative_mouse_model.dart new file mode 100644 index 000000000..2673cb8ae --- /dev/null +++ b/flutter/lib/models/relative_mouse_model.dart @@ -0,0 +1,1061 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:math' as math; +import 'dart:ui' as ui; + +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_hbb/main.dart'; +import 'package:flutter_hbb/utils/relative_mouse_accumulator.dart'; +import 'package:get/get.dart'; + +import '../common.dart'; +import '../consts.dart'; +import 'platform_model.dart'; + +class RelativeMouseModel { + final SessionID sessionId; + final RxBool enabled; + + final bool Function() keyboardPerm; + final bool Function() isViewCamera; + final String Function() peerVersion; + final String? Function() peerPlatform; + + final Map Function(Map msg) modify; + + final bool Function() getPointerInsideImage; + final void Function(bool inside) setPointerInsideImage; + + RelativeMouseModel({ + required this.sessionId, + required this.enabled, + required this.keyboardPerm, + required this.isViewCamera, + required this.peerVersion, + required this.peerPlatform, + required this.modify, + required this.getPointerInsideImage, + required this.setPointerInsideImage, + }); + + final RelativeMouseAccumulator _accumulator = RelativeMouseAccumulator(); + + // Native relative mouse mode support (macOS only) + // Uses CGAssociateMouseAndMouseCursorPosition to lock cursor and NSEvent monitor for raw delta. + static MethodChannel? _hostChannel; + // The currently active model receiving native mouse delta events. + // Note: Race condition between multiple sessions is not a concern here because + // when relative mouse mode is active, the cursor is locked and the user cannot + // switch to another session window. The user must first exit relative mouse mode + // (via Cmd+G on macOS or Ctrl+Alt on Windows/Linux) before they can interact + // with a different session. + static RelativeMouseModel? _activeNativeModel; + static bool _hostChannelInitialized = false; + + /// Initialize the host channel for native relative mouse mode. + /// This should be called once when the app starts on macOS. + static void initHostChannel() { + if (!isMacOS) return; + if (_hostChannelInitialized) return; + _hostChannelInitialized = true; + + _hostChannel = const MethodChannel('org.rustdesk.rustdesk/host'); + _hostChannel!.setMethodCallHandler((call) async { + if (call.method == 'onMouseDelta') { + final args = call.arguments as Map; + final dx = args['dx'] as int; + final dy = args['dy'] as int; + _activeNativeModel?._onNativeMouseDelta(dx, dy); + } + return null; + }); + } + + // TODO(perf): Consider routing native delta through RelativeMouseAccumulator/throttle + // if high-polling mice (e.g. 1000Hz+) cause message flooding on the network. + void _onNativeMouseDelta(int dx, int dy) { + if (!enabled.value) return; + // Send directly to remote without accumulator (native already provides integer deltas) + _sendMouseMessageToSession({ + 'type': 'move_relative', + 'x': '$dx', + 'y': '$dy', + }); + } + + Future _enableNativeRelativeMouseMode() async { + if (!isMacOS) return false; + if (_hostChannel == null) { + initHostChannel(); + if (_hostChannel == null) return false; + } + + // Defensive guard: prevent overwriting an already-active native session. + // In practice, this should not happen because when relative mouse mode is active, + // the cursor is locked and the user cannot switch to another session window. + // The user must first exit relative mouse mode (via Cmd+G on macOS or Ctrl+Alt on + // Windows/Linux) before interacting with a different session. + if (_activeNativeModel != null && _activeNativeModel != this) { + debugPrint( + '[RelMouse] Another model already has native relative mouse mode active'); + return false; + } + + try { + final result = + await _hostChannel!.invokeMethod('enableNativeRelativeMouseMode'); + if (result == true) { + _activeNativeModel = this; + return true; + } + } catch (e) { + debugPrint('[RelMouse] Failed to enable native relative mouse mode: $e'); + } + return false; + } + + Future _disableNativeRelativeMouseMode() async { + if (!isMacOS) return; + if (_hostChannel == null) return; + + // Only the owning model should disable native mode to avoid + // one session inadvertently disrupting another's native relative mouse state. + if (_activeNativeModel != this) { + return; + } + + try { + await _hostChannel!.invokeMethod('disableNativeRelativeMouseMode'); + } catch (e) { + debugPrint('[RelMouse] Failed to disable native relative mouse mode: $e'); + } finally { + if (_activeNativeModel == this) { + _activeNativeModel = null; + } + } + } + + // Whether native relative mouse mode is currently active for this model + bool get _isNativeRelativeMouseModeActive => + isMacOS && _activeNativeModel == this; + + // Pointer lock center in LOCAL widget coordinates (for delta calculation) + Offset? _pointerLockCenterLocal; + // Pointer lock center in SCREEN coordinates (for OS cursor re-centering) + Offset? _pointerLockCenterScreen; + // Pointer region top-left in Flutter view coordinates. + // Computed from PointerEvent.position - PointerEvent.localPosition. + Offset? _pointerRegionTopLeftGlobal; + // Last pointer position in LOCAL widget coordinates (fallback when center is not ready). + Offset? _lastPointerLocalPos; + + // Track whether we currently have an OS-level cursor clip active (Windows only). + // TODO(accuracy): Revisit window/client/border clipping math if users report misaligned + // clipping on custom or maximized window decorations. Consider using platform APIs + // (e.g. GetClientRect on Windows) instead of Flutter's window coordinates. + bool _cursorClipApplied = false; + + // Track whether a recenter operation is in progress to prevent overlapping calls. + bool _recenterInProgress = false; + + // Request token for async enable operation to prevent stale callbacks. + // Incremented on each enable attempt, callbacks check if token still matches. + int _enableRequestId = 0; + + // Throttle buffer for batching mouse move messages (reduces network flooding). + int _pendingDeltaX = 0; + int _pendingDeltaY = 0; + Timer? _throttleTimer; + static const Duration _throttleInterval = Duration(milliseconds: 16); + + // Size of the remote image widget (for center calculation) + Size? _imageWidgetSize; + + // Debounce timestamp for relative mouse mode toggle to prevent race conditions + // between Rust rdev grab loop and Flutter keyboard handling. + DateTime? _lastToggle; + + // Track key down state for exit shortcut. + // macOS: Cmd+G - track G key + // Windows/Linux: Ctrl+Alt - track whichever modifier was pressed last + // When key down is blocked (shortcut triggered), we also need to block + // the corresponding key up to avoid orphan key up events being sent to remote. + bool _exitShortcutKeyDown = false; + + // Callback to cancel external throttle timer when relative mouse mode is disabled. + VoidCallback? onDisabled; + + bool get isSupported { + // On Linux/Wayland, cursor warping is not supported, hide the option entirely. + if (isDesktop && isLinux && bind.mainCurrentIsWayland()) { + return false; + } + // Relative mouse mode is unsupported on remote Linux: + // 1. Long-press key events are unsupported. + // 2. The Wayland display server lacks cursor warping support. + final platform = peerPlatform(); + if (platform == kPeerPlatformLinux) { + return false; + } + final v = peerVersion(); + if (v.isEmpty) return false; + return versionCmp(v, kMinVersionForRelativeMouseMode) >= 0; + } + + Size? get imageWidgetSize => _imageWidgetSize; + + void updateImageWidgetSize(Size size) { + _imageWidgetSize = size; + if (enabled.value) { + _pointerLockCenterLocal = Offset(size.width / 2, size.height / 2); + } + } + + void updatePointerRegionTopLeftGlobal(PointerEvent e) { + _pointerRegionTopLeftGlobal = e.position - e.localPosition; + } + + /// Shared helper for handling exit shortcut for relative mouse mode. + /// Returns true if the event was handled and should not be forwarded. + /// + /// Exit shortcuts (only work when relative mouse mode is active): + /// - macOS: Cmd+G + /// - Windows/Linux: Ctrl+Alt (any order - triggered when both are pressed) + /// + /// [logicalKey] - the logical key of the event + /// [isKeyUp] - whether the event is a key up event + /// [isKeyDown] - whether the event is a key down event + /// [ctrlPressed], [altPressed], [commandPressed] - modifier states + bool _handleExitShortcut({ + required LogicalKeyboardKey logicalKey, + required bool isKeyUp, + required bool isKeyDown, + required bool ctrlPressed, + required bool altPressed, + required bool commandPressed, + }) { + if (!isDesktop || !keyboardPerm() || isViewCamera()) return false; + + // Only handle exit shortcuts when relative mouse mode is active + if (!enabled.value) return false; + + // Block key up if key down was blocked (to avoid orphan key up event on remote). + if (isKeyUp && _exitShortcutKeyDown) { + _exitShortcutKeyDown = false; + return true; + } + + if (!isKeyDown) return false; + + // macOS: Cmd+G to exit + if (isMacOS) { + final isGKey = logicalKey == LogicalKeyboardKey.keyG; + if (isGKey && commandPressed) { + _exitShortcutKeyDown = true; + setRelativeMouseMode(false); + return true; + } + return false; + } + + // Windows/Linux: Ctrl+Alt to exit + // Triggered when both modifiers are pressed (check on either Ctrl or Alt key down) + final isCtrlKey = logicalKey == LogicalKeyboardKey.controlLeft || + logicalKey == LogicalKeyboardKey.controlRight; + final isAltKey = logicalKey == LogicalKeyboardKey.altLeft || + logicalKey == LogicalKeyboardKey.altRight; + + // When Ctrl is pressed and Alt is already down, or vice versa + if ((isCtrlKey && altPressed) || (isAltKey && ctrlPressed)) { + _exitShortcutKeyDown = true; + setRelativeMouseMode(false); + return true; + } + + return false; + } + + bool handleKeyEvent( + KeyEvent e, { + required bool ctrlPressed, + required bool shiftPressed, + required bool altPressed, + required bool commandPressed, + }) { + return _handleExitShortcut( + logicalKey: e.logicalKey, + isKeyUp: e is KeyUpEvent, + isKeyDown: e is KeyDownEvent, + ctrlPressed: ctrlPressed, + altPressed: altPressed, + commandPressed: commandPressed, + ); + } + + /// Handle raw key events for relative mouse mode. + /// Returns true if the event was handled and should not be forwarded. + bool handleRawKeyEvent(RawKeyEvent e) { + final modifiers = e.data; + return _handleExitShortcut( + logicalKey: e.logicalKey, + isKeyUp: e is RawKeyUpEvent, + isKeyDown: e is RawKeyDownEvent, + ctrlPressed: modifiers.isControlPressed, + altPressed: modifiers.isAltPressed, + commandPressed: modifiers.isMetaPressed, + ); + } + + void onEnterOrLeaveImage(bool enter) { + if (!enabled.value) return; + + // Keep the shared pointer-in-image flag in sync. + setPointerInsideImage(enter); + + // macOS native mode: cursor is locked by CGAssociateMouseAndMouseCursorPosition, + // no need for recenter logic. + if (_isNativeRelativeMouseModeActive) { + return; + } + + if (!enter) { + _releaseCursorClip(); + return; + } + + // Windows: clip cursor to window rect + // Linux: use recenter method + updatePointerLockCenter().then((_) { + _recenterMouse(); + }); + } + + void onWindowBlur() { + if (!enabled.value) return; + + // Focus can change while the pointer is outside the window (e.g. taskbar activation). + // Do not rely on the previous "pointer inside" state across focus boundaries. + setPointerInsideImage(false); + // macOS native mode: don't call _releaseCursorClip as it would break CGAssociateMouseAndMouseCursorPosition + if (!_isNativeRelativeMouseModeActive) { + _releaseCursorClip(); + } + } + + void onWindowFocus() { + if (!enabled.value) return; + + // macOS native mode: cursor is already locked + if (_isNativeRelativeMouseModeActive) { + setPointerInsideImage(false); + return; + } + + // Guard: image widget size must be available for proper center calculation. + if (_imageWidgetSize == null) { + _disableWithCleanup(); + return; + } + + // Fail-safe: keep cursor usable on focus gain. Pointer lock will be re-engaged + // on the next pointer enter/move/hover inside the remote image. + setPointerInsideImage(false); + _releaseCursorClip(); + + // Best-effort: refresh center so the next engage is immediate. + updatePointerLockCenter(); + } + + void toggleRelativeMouseMode() { + final now = DateTime.now(); + if (_lastToggle != null && + now.difference(_lastToggle!).inMilliseconds < + kRelativeMouseModeToggleDebounceMs) { + return; + } + _lastToggle = now; + setRelativeMouseMode(!enabled.value); + } + + bool setRelativeMouseMode(bool value) { + // Web is not supported due to Pointer Lock API integration complexity with Flutter's input system + if (isWeb) { + return false; + } + + if (value) { + if (!keyboardPerm() || isViewCamera()) { + return false; + } + + if (isDesktop && _imageWidgetSize == null) { + // Desktop only: Ensure image widget size is available for proper center calculation. + showToast(translate('rel-mouse-not-ready-tip')); + return false; + } + + if (!isSupported) { + // Check server version support before enabling. + showToast(translate('rel-mouse-not-supported-peer-tip')); + return false; + } + } + + if (value) { + try { + if (isDesktop) { + final requestId = ++_enableRequestId; + if (isMacOS) { + // macOS: Use native relative mouse mode with CGAssociateMouseAndMouseCursorPosition + // This locks the cursor in place and provides raw delta via NSEvent monitor. + _enableNativeRelativeMouseMode().then((success) { + // Guard against stale callback: user may have toggled off relative mode + // while the async enable was in progress. + if (_enableRequestId != requestId) { + return; + } + if (success) { + _completeEnableRelativeMouseMode(); + } + // Note: _enableNativeRelativeMouseMode already handles its own cleanup on failure + }); + } else { + // Windows/Linux: Use Flutter-based cursor recenter approach + if (!getPointerInsideImage()) { + _releaseCursorClip(); + } + + updatePointerLockCenter().then((_) => _recenterMouse()).then((_) { + if (_enableRequestId != requestId) { + return; + } + _completeEnableRelativeMouseMode(); + }).catchError((e) { + if (_enableRequestId != requestId) { + return; + } + debugPrint('[RelMouse] Platform setup failed: $e'); + _resetState(); + }); + } + } else { + // Mobile: enable immediately (no platform-specific setup needed) + _completeEnableRelativeMouseMode(); + } + } catch (e) { + _disableWithCleanup(); + return false; + } + } else { + // Best-effort marker for Rust rdev grab loop (ESC behavior). + // Bypass keyboardPerm check to ensure Rust state is always synced, + // even if permission was revoked while relative mode was active. + _sendMouseMessageToSession( + { + 'relative_mouse_mode': '0', + }, + disableRelativeOnError: false, + bypassKeyboardPerm: true, + ); + + // Desktop only: cursor manipulation + if (isDesktop) { + if (isMacOS) { + // macOS: Disable native relative mouse mode + // This already calls CGAssociateMouseAndMouseCursorPosition(1) to re-associate mouse + _disableNativeRelativeMouseMode(); + } else { + _releaseCursorClip(); + } + } + enabled.value = false; + _resetState(); + onDisabled?.call(); + } + + return true; + } + + /// Called when platform setup completes successfully to finalize enabling relative mouse mode. + void _completeEnableRelativeMouseMode() { + enabled.value = true; + + // Show toast notification so user knows how to exit relative mouse mode (desktop only). + if (isDesktop) { + showToast( + translate('rel-mouse-exit-{${isMacOS ? "Cmd+G" : "Ctrl+Alt"}}-tip'), + alignment: Alignment.center); + } + + // Best-effort marker for Rust rdev grab loop (ESC behavior) and peer/server state. + // This uses a no-op delta so it does not move the remote cursor. + // Intentionally fire-and-forget: we don't block enabling on this marker message. + // Failures are logged but do not disable relative mouse mode. + _sendMouseMessageToSession( + { + 'relative_mouse_mode': '1', + 'type': 'move_relative', + 'x': '0', + 'y': '0', + }, + disableRelativeOnError: false, + ).catchError((e) { + debugPrint('[RelMouse] Failed to send enable marker: $e'); + return false; + }); + } + + // Flag to skip the first mouse move event after recenter (it's the recenter itself). + bool _skipNextMouseMove = false; + + /// Handle relative mouse movement based on current local pointer position. + /// Returns true if the event was handled in relative mode, false otherwise. + bool handleRelativeMouseMove(Offset localPosition) { + if (!enabled.value) return false; + + // macOS: Native mode handles delta via callback, skip Flutter-based handling. + if (_isNativeRelativeMouseModeActive) { + return true; + } + + // Pointer move/hover implies we're inside the remote image. + _ensurePointerLockEngaged(); + + // Skip the mouse move event triggered by recenter operation itself. + if (_skipNextMouseMove) { + _skipNextMouseMove = false; + _lastPointerLocalPos = localPosition; + return true; + } + + final lastLocal = _lastPointerLocalPos; + _lastPointerLocalPos = localPosition; + + // Linux-specific: Proactive recenter check before processing delta. + // On Linux, we don't have clip_cursor, so if the cursor moves too fast + // it may escape the window before _recenterIfNearEdge can catch it. + // Check now and recenter immediately if needed. + if (isLinux) { + _recenterIfNearEdgeLinux(localPosition); + } + + // Calculate delta from last position (not from center). + // This avoids issues with CGWarpMouseCursorPosition integer rounding. + if (lastLocal != null) { + final delta = localPosition - lastLocal; + if (delta.dx != 0 || delta.dy != 0) { + sendRelativeMouseMove(delta.dx, delta.dy); + } + } + + return true; + } + + /// Linux-specific: More aggressive recenter check to prevent cursor escape. + /// Called synchronously before processing mouse delta to ensure cursor stays within bounds. + void _recenterIfNearEdgeLinux(Offset localPosition) { + final size = _imageWidgetSize; + if (size == null) return; + + final edgeThreshold = _calculateEdgeThreshold(size); + + final nearLeft = localPosition.dx < edgeThreshold; + final nearRight = localPosition.dx > size.width - edgeThreshold; + final nearTop = localPosition.dy < edgeThreshold; + final nearBottom = localPosition.dy > size.height - edgeThreshold; + + if (nearLeft || nearRight || nearTop || nearBottom) { + _recenterMouse(); + } + } + + void sendRelativeMouseMove(double dx, double dy) { + if (!isDesktop) return; + + final delta = _accumulator.add(dx, dy, maxDelta: kMaxRelativeMouseDelta); + if (delta == null) return; + + // Buffer the delta for throttled sending. + _pendingDeltaX += delta.x; + _pendingDeltaY += delta.y; + + // Start or refresh the throttle timer. + if (_throttleTimer == null || !_throttleTimer!.isActive) { + _throttleTimer = Timer(_throttleInterval, () => _flushPendingDelta()); + } + } + + Future _flushPendingDelta() async { + if (!isDesktop) return; + if (_pendingDeltaX == 0 && _pendingDeltaY == 0) return; + + final x = _pendingDeltaX; + final y = _pendingDeltaY; + _pendingDeltaX = 0; + _pendingDeltaY = 0; + + final ok = await _sendMouseMessageToSession({ + 'type': 'move_relative', + 'x': '$x', + 'y': '$y', + }); + if (!ok) return; + + // Only recenter when mouse is near the edge of the image widget. + // This allows smooth mouse movement without constant recentering. + _recenterIfNearEdge(); + } + + // Edge threshold parameters for recenter detection. + // Threshold is calculated as: min(maxThreshold, min(width, height) * fraction) + static const double _edgeThresholdFraction = 0.1; // 10% of smaller dimension + static const double _edgeThresholdMax = + 100.0; // Maximum threshold in logical pixels + static const double _edgeThresholdMin = + 20.0; // Minimum threshold for very small widgets + + // Linux-specific edge threshold parameters (more aggressive to prevent cursor escape). + // On Linux, we don't have clip_cursor capability, so we need to recenter earlier + // to prevent the cursor from escaping the window when moving fast. + static const double _edgeThresholdFractionLinux = + 0.25; // 25% of smaller dimension + static const double _edgeThresholdMaxLinux = + 200.0; // Larger maximum threshold for Linux + static const double _edgeThresholdMinLinux = + 50.0; // Larger minimum threshold for Linux + + /// Calculate dynamic edge threshold based on widget size. + double _calculateEdgeThreshold(Size size) { + final smallerDimension = math.min(size.width, size.height); + if (isLinux) { + // Use more aggressive thresholds on Linux to prevent cursor escape. + final dynamicThreshold = smallerDimension * _edgeThresholdFractionLinux; + return dynamicThreshold.clamp( + _edgeThresholdMinLinux, _edgeThresholdMaxLinux); + } + final dynamicThreshold = smallerDimension * _edgeThresholdFraction; + // Clamp between min and max thresholds + return dynamicThreshold.clamp(_edgeThresholdMin, _edgeThresholdMax); + } + + /// Recenter the cursor only if it's near the edge of the image widget. + void _recenterIfNearEdge() { + final lastPos = _lastPointerLocalPos; + final size = _imageWidgetSize; + if (lastPos == null || size == null) return; + + // Dynamic threshold based on widget size + final edgeThreshold = _calculateEdgeThreshold(size); + + final nearLeft = lastPos.dx < edgeThreshold; + final nearRight = lastPos.dx > size.width - edgeThreshold; + final nearTop = lastPos.dy < edgeThreshold; + final nearBottom = lastPos.dy > size.height - edgeThreshold; + + if (nearLeft || nearRight || nearTop || nearBottom) { + _recenterMouse(); + } + } + + /// Send mouse button event without position (for relative mouse mode). + Future sendRelativeMouseButton(Map evt) async { + if (!enabled.value) return; + _ensurePointerLockEngaged(); + + final rawType = evt['type']; + final rawButtons = evt['buttons']; + if (rawType is! String || rawButtons is! int) return; + + final type = _mouseEventTypeToPeer(rawType); + if (type.isEmpty) return; + + final buttons = mouseButtonsToPeer(rawButtons); + if (buttons.isEmpty) return; + + await _sendMouseMessageToSession({ + 'type': type, + 'buttons': buttons, + }); + } + + static String _mouseEventTypeToPeer(String type) { + switch (type) { + case 'mousedown': + return kMouseEventTypeDown; + case 'mouseup': + return kMouseEventTypeUp; + default: + return ''; + } + } + + Future _sendMouseMessageToSession( + Map msg, { + bool disableRelativeOnError = true, + bool bypassKeyboardPerm = false, + }) async { + if (!bypassKeyboardPerm && !keyboardPerm()) return false; + if (isViewCamera()) return false; + + try { + await bind.sessionSendMouse( + sessionId: sessionId, + msg: json.encode(modify(msg)), + ); + return true; + } catch (e) { + debugPrint('[RelMouse] Error sending mouse message: $e'); + if (disableRelativeOnError && enabled.value) { + _disableWithCleanup(); + } + return false; + } + } + + /// Retry parameters for cursor re-centering. + static const int _recenterMaxRetries = 3; + static const Duration _recenterRetryDelay = Duration(milliseconds: 100); + + /// Recenter the cursor to the pointer lock center. + /// Fire-and-forget safe: prevents overlapping calls and catches errors internally. + Future _recenterMouse() async { + // Prevent overlapping recenter operations under high-frequency mouse moves. + if (_recenterInProgress) return; + _recenterInProgress = true; + + try { + if (!enabled.value) return; + if (!getPointerInsideImage()) return; + + final center = _pointerLockCenterScreen; + if (center == null) { + return; + } + + for (int attempt = 0; attempt < _recenterMaxRetries; attempt++) { + // Check preconditions before each attempt. + if (!enabled.value || !getPointerInsideImage()) return; + + final ok = bind.mainSetCursorPosition( + x: center.dx.toInt(), + y: center.dy.toInt(), + ); + if (ok) { + // Skip the next mouse move event - it's triggered by the recenter itself. + _skipNextMouseMove = true; + return; + } + + // Wait before retrying (except on the last attempt). + if (attempt < _recenterMaxRetries - 1) { + await Future.delayed(_recenterRetryDelay); + } + } + + // All attempts failed. + _disableWithCleanup(); + showToast(translate('rel-mouse-lock-failed-tip')); + } catch (e, st) { + debugPrint('[RelMouse] Unexpected error in _recenterMouse: $e\n$st'); + } finally { + _recenterInProgress = false; + } + } + + Future updatePointerLockCenter({Offset? localCenter}) async { + if (!isDesktop) return; + + // Null safety check for kWindowId. + if (kWindowId == null) { + if (enabled.value) { + _disableWithCleanup(); + } + return; + } + + try { + final wc = WindowController.fromWindowId(kWindowId!); + final frame = await wc.getFrame(); + + if (frame.width <= 0 || frame.height <= 0) { + if (enabled.value) { + _disableWithCleanup(); + } + return; + } + + if (localCenter != null) { + _pointerLockCenterLocal = localCenter; + } else if (_imageWidgetSize != null) { + _pointerLockCenterLocal = Offset( + _imageWidgetSize!.width / 2, + _imageWidgetSize!.height / 2, + ); + } else { + if (enabled.value) { + _disableWithCleanup(); + } + return; + } + + // Calculate screen coordinates for OS cursor positioning. + // Use PlatformDispatcher instead of deprecated ui.window. + final view = ui.PlatformDispatcher.instance.views.firstOrNull; + if (view == null) { + debugPrint('[RelMouse] No view available for coordinate calculation'); + if (enabled.value) { + _disableWithCleanup(); + } + return; + } + final scale = view.devicePixelRatio; + + if (_pointerRegionTopLeftGlobal != null && scale > 0) { + // On macOS, window frame and CGWarpMouseCursorPosition use points (not pixels). + // On Windows, they use pixels. + // Flutter's logical coordinates are in points on macOS. + final centerInView = + _pointerRegionTopLeftGlobal! + _pointerLockCenterLocal!; + + // Calculate client area offset (excluding title bar and borders) + final clientPhysical = view.physicalSize; + + // macOS: Window frame and CGWarpMouseCursorPosition both use points (not pixels). + // We convert clientPhysical (pixels) to points via `/ scale` to compute titleBarHeight, + // which is the difference between the total window height and the Flutter view height. + if (isMacOS) { + final clientHeightPoints = clientPhysical.height / scale; + final titleBarHeight = frame.height - clientHeightPoints; + + _pointerLockCenterScreen = Offset( + frame.left + centerInView.dx, + frame.top + titleBarHeight + centerInView.dy, + ); + } else { + // Windows/Linux: Use pixel coordinates. We estimate the client-area offset using + // a heuristic based on the difference between frame size and client physical size. + // This assumes symmetric horizontal borders (extraW / 2) and that the remaining + // vertical space (extraH - borderBottom) is the title bar height. + // Limitation: This heuristic may be inaccurate for maximized windows, custom window + // decorations, or when the OS uses different border styles. + // TODO: Replace this heuristic with platform API calls (e.g., GetClientRect on Windows) + // if precise client-area offsets are required. + final extraW = frame.width - clientPhysical.width; + final extraH = frame.height - clientPhysical.height; + final borderX = extraW > 0 ? extraW / 2 : 0.0; + final borderBottom = borderX; + final borderTop = extraH > borderBottom ? extraH - borderBottom : 0.0; + final clientTopLeftScreen = + Offset(frame.left + borderX, frame.top + borderTop); + + // Calculate tentative center, then validate it's within frame bounds. + // This guards against heuristic inaccuracies (e.g., maximized windows). + final tentativeCenter = Offset( + clientTopLeftScreen.dx + centerInView.dx * scale, + clientTopLeftScreen.dy + centerInView.dy * scale, + ); + final withinFrame = tentativeCenter.dx >= frame.left && + tentativeCenter.dx <= frame.left + frame.width && + tentativeCenter.dy >= frame.top && + tentativeCenter.dy <= frame.top + frame.height; + _pointerLockCenterScreen = withinFrame + ? tentativeCenter + : Offset( + frame.left + frame.width / 2, frame.top + frame.height / 2); + } + } else { + _pointerLockCenterScreen = Offset( + frame.left + frame.width / 2, + frame.top + frame.height / 2, + ); + } + + if (enabled.value && isWindows && getPointerInsideImage()) { + _applyCursorClipForFrame(frame); + } else if (enabled.value && isWindows && _cursorClipApplied) { + // Only release if we actually have a clip applied to avoid redundant FFI calls. + _releaseCursorClip(); + } + // macOS: no clip_cursor (CGAssociateMouseAndMouseCursorPosition stops mouse events) + // Instead, we use recenter method like other platforms. + } catch (e) { + if (enabled.value) { + _disableWithCleanup(); + } else { + _pointerLockCenterLocal = null; + _pointerLockCenterScreen = null; + } + } + } + + void _ensurePointerLockEngaged() { + if (!enabled.value) return; + if (!isDesktop) return; + + setPointerInsideImage(true); + + final needsCenter = + _pointerLockCenterLocal == null || _pointerLockCenterScreen == null; + // Windows only: cursor clip + final needsClip = isWindows && !_cursorClipApplied; + if (needsCenter || needsClip) { + updatePointerLockCenter() + .then((_) => _recenterMouse()) + .catchError((Object e, StackTrace st) { + debugPrint('[RelMouse] updatePointerLockCenter failed: $e\n$st'); + _disableWithCleanup(); + }); + } + } + + void _applyCursorClipForFrame(Rect frame) { + if (!isWindows) return; + + // Use PlatformDispatcher to get the device pixel ratio for proper scaling. + final view = ui.PlatformDispatcher.instance.views.firstOrNull; + final scale = view?.devicePixelRatio ?? 1.0; + + // Get the Flutter view's physical size (client area in pixels). + final clientPhysical = view?.physicalSize ?? ui.Size.zero; + + // Calculate the non-client area (OS window title bar, borders). + // frame includes the entire window (title bar + borders + client area). + final extraW = frame.width - clientPhysical.width; + final extraH = frame.height - clientPhysical.height; + + // Assume symmetric horizontal borders. + final borderX = extraW > 0 ? extraW / 2 : 0.0; + // Bottom border is typically the same as side borders. + final borderBottom = borderX; + // OS window title bar height is the remaining vertical non-client space. + final borderTop = extraH > borderBottom ? extraH - borderBottom : 0.0; + + // Calculate client area top-left in screen coordinates. + final clientTopLeftScreen = + Offset(frame.left + borderX, frame.top + borderTop); + + int left, top, right, bottom; + + // If we have precise image widget info, clip to the remote image area. + // This excludes the Flutter app's internal title bar and toolbar. + if (_pointerRegionTopLeftGlobal != null && + _imageWidgetSize != null && + scale > 0) { + // _pointerRegionTopLeftGlobal is in Flutter logical coordinates (relative to client area). + // Convert to screen physical coordinates. + left = (clientTopLeftScreen.dx + _pointerRegionTopLeftGlobal!.dx * scale) + .toInt(); + top = (clientTopLeftScreen.dy + _pointerRegionTopLeftGlobal!.dy * scale) + .toInt(); + right = (left + _imageWidgetSize!.width * scale).toInt(); + bottom = (top + _imageWidgetSize!.height * scale).toInt(); + } else { + // Fallback: clip to client area (excluding OS window decorations). + left = clientTopLeftScreen.dx.toInt(); + top = clientTopLeftScreen.dy.toInt(); + right = (frame.left + frame.width - borderX).toInt(); + bottom = (frame.top + frame.height - borderBottom).toInt(); + } + + _cursorClipApplied = bind.mainClipCursor( + left: left, + top: top, + right: right, + bottom: bottom, + enable: true, + ); + } + + void _releaseCursorClip() { + if (!_cursorClipApplied) return; + _cursorClipApplied = false; + if (!isWindows) return; + + bind.mainClipCursor( + left: 0, + top: 0, + right: 0, + bottom: 0, + enable: false, + ); + } + + void _resetState() { + // Flush any pending delta before clearing state. + // This ensures the last buffered movement is sent before values are zeroed. + // Fire-and-forget: we don't wait for the async send to complete. + if (_throttleTimer != null || _pendingDeltaX != 0 || _pendingDeltaY != 0) { + _throttleTimer?.cancel(); + _throttleTimer = null; + if (_pendingDeltaX != 0 || _pendingDeltaY != 0) { + final x = _pendingDeltaX; + final y = _pendingDeltaY; + _pendingDeltaX = 0; + _pendingDeltaY = 0; + // Send without awaiting; skip recenter since we're disabling. + _sendMouseMessageToSession({ + 'type': 'move_relative', + 'x': '$x', + 'y': '$y', + }, disableRelativeOnError: false); + } + } + _accumulator.reset(); + _pointerLockCenterLocal = null; + _pointerLockCenterScreen = null; + _pointerRegionTopLeftGlobal = null; + _lastPointerLocalPos = null; + _skipNextMouseMove = false; + setPointerInsideImage(false); + _cursorClipApplied = false; + _exitShortcutKeyDown = false; + } + + /// Core cleanup logic shared by [_disableWithCleanup] and [dispose]. + /// Sends disable message to Rust, releases platform resources, and resets state. + void _performCleanupCore() { + // Best-effort marker for Rust rdev grab loop (ESC behavior). + // Bypass keyboardPerm check to ensure Rust state is always synced. + _sendMouseMessageToSession( + { + 'relative_mouse_mode': '0', + }, + disableRelativeOnError: false, + bypassKeyboardPerm: true, + ); + + // macOS: Disable native relative mouse mode + // This already calls CGAssociateMouseAndMouseCursorPosition(1) to re-associate mouse + if (isMacOS) { + _disableNativeRelativeMouseMode(); + } else { + _releaseCursorClip(); + } + + _resetState(); + } + + void _disableWithCleanup() { + _performCleanupCore(); + enabled.value = false; + onDisabled?.call(); + } + + bool _disposed = false; + + void dispose() { + if (_disposed) return; + _disposed = true; + + _performCleanupCore(); + _imageWidgetSize = null; + _lastToggle = null; + // Set enabled to false BEFORE calling onDisabled, consistent with _disableWithCleanup(). + enabled.value = false; + // Trigger callback before clearing it, so external cleanup can run. + onDisabled?.call(); + onDisabled = null; + } +} diff --git a/flutter/lib/models/state_model.dart b/flutter/lib/models/state_model.dart index 2e1b516df..77195d662 100644 --- a/flutter/lib/models/state_model.dart +++ b/flutter/lib/models/state_model.dart @@ -1,5 +1,4 @@ import 'package:desktop_multi_window/desktop_multi_window.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:get/get.dart'; @@ -30,6 +29,11 @@ class StateGlobal { String _inputSource = ''; + // Track relative mouse mode state for each peer connection. + // Key: peerId, Value: true if relative mouse mode is active. + // Note: This is session-only runtime state, NOT persisted to config. + final RxMap relativeMouseModeState = {}.obs; + // Use for desktop -> remote toolbar -> resolution final Map> _lastResolutionGroupValues = {}; diff --git a/flutter/lib/utils/relative_mouse_accumulator.dart b/flutter/lib/utils/relative_mouse_accumulator.dart new file mode 100644 index 000000000..0b1426449 --- /dev/null +++ b/flutter/lib/utils/relative_mouse_accumulator.dart @@ -0,0 +1,58 @@ +/// A small helper for accumulating fractional mouse deltas and emitting integer deltas. +/// +/// Relative mouse mode uses integer deltas on the wire, but Flutter pointer deltas +/// are doubles. This accumulator preserves sub-pixel movement by carrying the +/// fractional remainder across events. +class RelativeMouseDelta { + final int x; + final int y; + + const RelativeMouseDelta(this.x, this.y); +} + +/// Accumulates fractional mouse deltas and returns integer deltas when available. +class RelativeMouseAccumulator { + double _fracX = 0.0; + double _fracY = 0.0; + + /// Adds a delta and returns an integer delta when at least one axis reaches a + /// magnitude of 1px (after truncation towards zero). + /// + /// If [maxDelta] is > 0, the returned integer delta is clamped to + /// [-maxDelta, maxDelta] on each axis. + RelativeMouseDelta? add( + double dx, + double dy, { + required int maxDelta, + }) { + // Guard against misuse: negative maxDelta would silently disable clamping. + assert(maxDelta >= 0, 'maxDelta must be non-negative'); + + _fracX += dx; + _fracY += dy; + + int intX = _fracX.truncate(); + int intY = _fracY.truncate(); + + if (intX == 0 && intY == 0) { + return null; + } + + // Clamp before subtracting so excess movement is preserved in the accumulator + // rather than being permanently discarded during spikes. + if (maxDelta > 0) { + intX = intX.clamp(-maxDelta, maxDelta); + intY = intY.clamp(-maxDelta, maxDelta); + } + + _fracX -= intX; + _fracY -= intY; + + return RelativeMouseDelta(intX, intY); + } + + void reset() { + _fracX = 0.0; + _fracY = 0.0; + } +} diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index d703a4dca..4a4e89233 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -2020,5 +2020,19 @@ class RustdeskImpl { return js.context.callMethod('getByName', ['audit_guid']); } + bool mainSetCursorPosition({required int x, required int y, dynamic hint}) { + return false; + } + + bool mainClipCursor( + {required int left, + required int top, + required int right, + required int bottom, + required bool enable, + dynamic hint}) { + return false; + } + void dispose() {} } diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift index d27d7f228..1cc72419b 100644 --- a/flutter/macos/Runner/MainFlutterWindow.swift +++ b/flutter/macos/Runner/MainFlutterWindow.swift @@ -19,6 +19,22 @@ import window_manager import window_size import texture_rgba_renderer +// Global state for relative mouse mode +// All properties and methods must be accessed on the main thread since they +// interact with NSEvent monitors, CoreGraphics APIs, and Flutter channels. +// Note: We avoid @MainActor to maintain macOS 10.14 compatibility. +class RelativeMouseState { + static let shared = RelativeMouseState() + + var enabled = false + var eventMonitor: Any? + var deltaChannel: FlutterMethodChannel? + var accumulatedDeltaX: CGFloat = 0 + var accumulatedDeltaY: CGFloat = 0 + + private init() {} +} + class MainFlutterWindow: NSWindow { override func awakeFromNib() { rustdesk_core_main(); @@ -64,6 +80,104 @@ class MainFlutterWindow: NSWindow { window.appearance = NSAppearance(named: themeName == "light" ? .aqua : .darkAqua) } + private func enableNativeRelativeMouseMode(channel: FlutterMethodChannel) -> Bool { + assert(Thread.isMainThread, "enableNativeRelativeMouseMode must be called on the main thread") + let state = RelativeMouseState.shared + if state.enabled { + // Already enabled: update the channel so this caller receives deltas. + state.deltaChannel = channel + return true + } + + // Dissociate mouse from cursor position - this locks the cursor in place + // Do this FIRST before setting any state + let result = CGAssociateMouseAndMouseCursorPosition(0) + if result != CGError.success { + NSLog("[RustDesk] Failed to dissociate mouse from cursor position: %d", result.rawValue) + return false + } + + // Only set state after CG call succeeds + state.deltaChannel = channel + state.accumulatedDeltaX = 0 + state.accumulatedDeltaY = 0 + + // Add local event monitor to capture mouse delta. + // Note: Local event monitors are always called on the main thread, + // so accessing main-thread-only state is safe here. + state.eventMonitor = NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved, .leftMouseDragged, .rightMouseDragged, .otherMouseDragged]) { [weak state] event in + guard let state = state else { return event } + // Guard against race: mode may be disabled between weak capture and this check. + guard state.enabled else { return event } + let deltaX = event.deltaX + let deltaY = event.deltaY + + if deltaX != 0 || deltaY != 0 { + // Accumulate delta (main thread only - NSEvent local monitors always run on main thread) + state.accumulatedDeltaX += deltaX + state.accumulatedDeltaY += deltaY + + // Only send if we have integer movement + let intX = Int(state.accumulatedDeltaX) + let intY = Int(state.accumulatedDeltaY) + + if intX != 0 || intY != 0 { + state.accumulatedDeltaX -= CGFloat(intX) + state.accumulatedDeltaY -= CGFloat(intY) + + // Send delta to Flutter (already on main thread) + state.deltaChannel?.invokeMethod("onMouseDelta", arguments: ["dx": intX, "dy": intY]) + } + } + + return event + } + + // Check if monitor was created successfully + if state.eventMonitor == nil { + NSLog("[RustDesk] Failed to create event monitor for relative mouse mode") + // Re-associate mouse since we failed + CGAssociateMouseAndMouseCursorPosition(1) + state.deltaChannel = nil + return false + } + + // Set enabled LAST after everything succeeds + state.enabled = true + return true + } + + private func disableNativeRelativeMouseMode() { + assert(Thread.isMainThread, "disableNativeRelativeMouseMode must be called on the main thread") + let state = RelativeMouseState.shared + if !state.enabled { return } + + state.enabled = false + + // Remove event monitor + if let monitor = state.eventMonitor { + NSEvent.removeMonitor(monitor) + state.eventMonitor = nil + } + + state.deltaChannel = nil + state.accumulatedDeltaX = 0 + state.accumulatedDeltaY = 0 + + // Re-associate mouse with cursor position (non-blocking with async retry) + let result = CGAssociateMouseAndMouseCursorPosition(1) + if result != CGError.success { + NSLog("[RustDesk] Failed to re-associate mouse with cursor position: %d, scheduling retry...", result.rawValue) + // Non-blocking retry after 50ms + DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { + let retryResult = CGAssociateMouseAndMouseCursorPosition(1) + if retryResult != CGError.success { + NSLog("[RustDesk] Retry failed to re-associate mouse: %d. Cursor may remain locked.", retryResult.rawValue) + } + } + } + } + public func setMethodHandler(registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: "org.rustdesk.rustdesk/host", binaryMessenger: registrar.messenger) channel.setMethodCallHandler({ @@ -96,7 +210,9 @@ class MainFlutterWindow: NSWindow { } case "requestRecordAudio": AVCaptureDevice.requestAccess(for: .audio, completionHandler: { granted in - result(granted) + DispatchQueue.main.async { + result(granted) + } }) break case "bumpMouse": @@ -145,11 +261,22 @@ class MainFlutterWindow: NSWindow { // This function's main action is to toggle whether the mouse cursor is // associated with the mouse position, but setting it to true when it's // already true has the side-effect of cancelling this motion suppression. - CGAssociateMouseAndMouseCursorPosition(1 /* true */) + // + // However, we must NOT call this when relative mouse mode is active, + // as it would break the pointer lock established by enableNativeRelativeMouseMode. + if !RelativeMouseState.shared.enabled { + CGAssociateMouseAndMouseCursorPosition(1 /* true */) + } result(true) - break + case "enableNativeRelativeMouseMode": + let success = self.enableNativeRelativeMouseMode(channel: channel) + result(success) + + case "disableNativeRelativeMouseMode": + self.disableNativeRelativeMouseMode() + result(true) default: result(FlutterMethodNotImplemented) diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 448eae4db..b8360db58 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers -version: 1.4.4+62 +version: 1.4.5+63 environment: sdk: '^3.1.0' diff --git a/libs/enigo/src/macos/macos_impl.rs b/libs/enigo/src/macos/macos_impl.rs index d85f3576f..20f5d0cbf 100644 --- a/libs/enigo/src/macos/macos_impl.rs +++ b/libs/enigo/src/macos/macos_impl.rs @@ -208,42 +208,56 @@ impl MouseControllable for Enigo { } fn mouse_move_to(&mut self, x: i32, y: i32) { - let pressed = Self::pressed_buttons(); - - let event_type = if pressed & 1 > 0 { - CGEventType::LeftMouseDragged - } else if pressed & 2 > 0 { - CGEventType::RightMouseDragged - } else { - CGEventType::MouseMoved - }; - - let dest = CGPoint::new(x as f64, y as f64); - if let Some(src) = self.event_source.as_ref() { - if let Ok(event) = - CGEvent::new_mouse_event(src.clone(), event_type, dest, CGMouseButton::Left) - { - self.post(event, None); - } - } + // For absolute movement, we don't set delta values + // This maintains backward compatibility + self.mouse_move_to_impl(x, y, None); } fn mouse_move_relative(&mut self, x: i32, y: i32) { let (display_width, display_height) = Self::main_display_size(); let (current_x, y_inv) = Self::mouse_location_raw_coords(); let current_y = (display_height as i32) - y_inv; - let new_x = current_x + x; - let new_y = current_y + y; + // Use saturating arithmetic to prevent overflow/wraparound + let mut new_x = current_x.saturating_add(x); + let mut new_y = current_y.saturating_add(y); - if new_x < 0 - || new_x as usize > display_width - || new_y < 0 - || new_y as usize > display_height - { - return; + // Define screen center and edge margins for cursor reset + let center_x = (display_width / 2) as i32; + let center_y = (display_height / 2) as i32; + // Margin calculation: 5% of the smaller screen dimension with a minimum of 50px. + // This provides a comfortable buffer zone to detect when the cursor is approaching + // screen edges, allowing us to reset it to center before it hits the boundary. + // This ensures continuous relative mouse movement without getting stuck at edges. + let margin = (display_width.min(display_height) / 20).max(50) as i32; + + // Check if cursor is approaching screen boundaries + // Use saturating_sub to prevent negative thresholds on very small displays + let right = (display_width as i32).saturating_sub(margin); + let bottom = (display_height as i32).saturating_sub(margin); + let near_edge = new_x < margin + || new_x > right + || new_y < margin + || new_y > bottom; + + if near_edge { + // Reset cursor to screen center to allow continuous movement + // The delta values are still passed correctly for games/apps + new_x = center_x; + new_y = center_y; } - self.mouse_move_to(new_x, new_y); + // Clamp to screen bounds as a safety measure. + // Use saturating_sub(1) to ensure coordinates don't exceed the last valid pixel. + let max_x = (display_width as i32).saturating_sub(1).max(0); + let max_y = (display_height as i32).saturating_sub(1).max(0); + new_x = new_x.clamp(0, max_x); + new_y = new_y.clamp(0, max_y); + + // Pass delta values for relative movement + // This is critical for browser Pointer Lock API support + // The delta fields (MOUSE_EVENT_DELTA_X/Y) are used by browsers + // to calculate movementX/Y in Pointer Lock mode + self.mouse_move_to_impl(new_x, new_y, Some((x, y))); } fn mouse_down(&mut self, button: MouseButton) -> crate::ResultType { @@ -473,6 +487,43 @@ impl Enigo { } } + /// Internal implementation for mouse movement with optional delta values. + /// + /// The `delta` parameter is crucial for browser Pointer Lock API support. + /// When a browser enters Pointer Lock mode, it reads mouse delta values + /// (MOUSE_EVENT_DELTA_X/Y) directly from CGEvent to calculate movementX/Y. + /// Without setting these fields, the browser sees zero movement. + fn mouse_move_to_impl(&mut self, x: i32, y: i32, delta: Option<(i32, i32)>) { + let pressed = Self::pressed_buttons(); + + // Determine event type and corresponding mouse button based on pressed buttons. + // The CGMouseButton must match the event type for drag events. + let (event_type, button) = if pressed & 1 > 0 { + (CGEventType::LeftMouseDragged, CGMouseButton::Left) + } else if pressed & 2 > 0 { + (CGEventType::RightMouseDragged, CGMouseButton::Right) + } else if pressed & 4 > 0 { + (CGEventType::OtherMouseDragged, CGMouseButton::Center) + } else { + (CGEventType::MouseMoved, CGMouseButton::Left) // Button doesn't matter for MouseMoved + }; + + let dest = CGPoint::new(x as f64, y as f64); + if let Some(src) = self.event_source.as_ref() { + if let Ok(event) = + CGEvent::new_mouse_event(src.clone(), event_type, dest, button) + { + // Set delta fields for relative mouse movement + // This is essential for Pointer Lock API in browsers + if let Some((dx, dy)) = delta { + event.set_integer_value_field(EventField::MOUSE_EVENT_DELTA_X, dx as i64); + event.set_integer_value_field(EventField::MOUSE_EVENT_DELTA_Y, dy as i64); + } + self.post(event, None); + } + } + } + /// Fetches the `(width, height)` in pixels of the main display pub fn main_display_size() -> (usize, usize) { let display_id = unsafe { CGMainDisplayID() }; diff --git a/libs/portable/Cargo.toml b/libs/portable/Cargo.toml index 00b47e976..a4a71e14f 100644 --- a/libs/portable/Cargo.toml +++ b/libs/portable/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk-portable-packer" -version = "1.4.4" +version = "1.4.5" edition = "2021" description = "RustDesk Remote Desktop" diff --git a/res/PKGBUILD b/res/PKGBUILD index bd890d1ed..3b4096760 100644 --- a/res/PKGBUILD +++ b/res/PKGBUILD @@ -1,5 +1,5 @@ pkgname=rustdesk -pkgver=1.4.4 +pkgver=1.4.5 pkgrel=0 epoch= pkgdesc="" diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec index 38a3fb12b..d11e0b69a 100644 --- a/res/rpm-flutter-suse.spec +++ b/res/rpm-flutter-suse.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.4.4 +Version: 1.4.5 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec index 192d31156..3b6ad5f5d 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.4.4 +Version: 1.4.5 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm.spec b/res/rpm.spec index b2162039d..67c7abe36 100644 --- a/res/rpm.spec +++ b/res/rpm.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.4.4 +Version: 1.4.5 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/src/common.rs b/src/common.rs index 66a12994d..5f8772414 100644 --- a/src/common.rs +++ b/src/common.rs @@ -71,6 +71,19 @@ pub mod input { pub const MOUSE_TYPE_UP: i32 = 2; pub const MOUSE_TYPE_WHEEL: i32 = 3; pub const MOUSE_TYPE_TRACKPAD: i32 = 4; + /// Relative mouse movement type for gaming/3D applications. + /// This type sends delta (dx, dy) values instead of absolute coordinates. + /// NOTE: This is only supported by the Flutter client. The Sciter client (deprecated) + /// does not support relative mouse mode due to: + /// 1. Fixed send_mouse() function signature that doesn't allow type differentiation + /// 2. Lack of pointer lock API in Sciter/TIS + /// 3. No OS cursor control (hide/show/clip) FFI bindings in Sciter UI + pub const MOUSE_TYPE_MOVE_RELATIVE: i32 = 5; + + /// Mask to extract the mouse event type from the mask field. + /// The lower 3 bits contain the event type (MOUSE_TYPE_*), giving a valid range of 0-7. + /// Currently defined types use values 0-5; values 6 and 7 are reserved for future use. + pub const MOUSE_TYPE_MASK: i32 = 0x7; pub const MOUSE_BUTTON_LEFT: i32 = 0x01; pub const MOUSE_BUTTON_RIGHT: i32 = 0x02; @@ -175,6 +188,20 @@ pub fn is_support_file_transfer_resume_num(ver: i64) -> bool { ver >= hbb_common::get_version_number("1.4.2") } +/// Minimum server version required for relative mouse mode support. +/// This constant must mirror Flutter's `kMinVersionForRelativeMouseMode` in `consts.dart`. +const MIN_VERSION_RELATIVE_MOUSE_MODE: &str = "1.4.5"; + +#[inline] +pub fn is_support_relative_mouse_mode(ver: &str) -> bool { + is_support_relative_mouse_mode_num(hbb_common::get_version_number(ver)) +} + +#[inline] +pub fn is_support_relative_mouse_mode_num(ver: i64) -> bool { + ver >= hbb_common::get_version_number(MIN_VERSION_RELATIVE_MOUSE_MODE) +} + // is server process, with "--server" args #[inline] pub fn is_server() -> bool { @@ -2462,4 +2489,36 @@ mod tests { assert!(!is_public("https://rustdesk.computer.com")); assert!(!is_public("rustdesk.comhello.com")); } + + #[test] + fn test_mouse_event_constants_and_mask_layout() { + use super::input::*; + + // Verify MOUSE_TYPE constants are unique and within the mask range. + let types = [ + MOUSE_TYPE_MOVE, + MOUSE_TYPE_DOWN, + MOUSE_TYPE_UP, + MOUSE_TYPE_WHEEL, + MOUSE_TYPE_TRACKPAD, + MOUSE_TYPE_MOVE_RELATIVE, + ]; + + let mut seen = std::collections::HashSet::new(); + for t in types.iter() { + assert!(seen.insert(*t), "Duplicate mouse type: {}", t); + assert_eq!( + *t & MOUSE_TYPE_MASK, + *t, + "Mouse type {} exceeds mask {}", + t, + MOUSE_TYPE_MASK + ); + } + + // The mask layout is: lower 3 bits for type, upper bits for buttons (shifted by 3). + let combined_mask = MOUSE_TYPE_DOWN | ((MOUSE_BUTTON_LEFT | MOUSE_BUTTON_RIGHT) << 3); + assert_eq!(combined_mask & MOUSE_TYPE_MASK, MOUSE_TYPE_DOWN); + assert_eq!(combined_mask >> 3, MOUSE_BUTTON_LEFT | MOUSE_BUTTON_RIGHT); + } } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index f2d3e34ef..a46cfd8b6 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1215,6 +1215,66 @@ pub fn main_set_input_source(session_id: SessionID, value: String) { } } +/// Set cursor position (for pointer lock re-centering). +/// +/// # Returns +/// - `true`: cursor position was successfully set +/// - `false`: operation failed or not supported +/// +/// # Platform behavior +/// - Windows/macOS/Linux: attempts to move the cursor to (x, y) +/// - Android/iOS: no-op, always returns `false` +pub fn main_set_cursor_position(x: i32, y: i32) -> SyncReturn { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + SyncReturn(crate::set_cursor_pos(x, y)) + } + #[cfg(any(target_os = "android", target_os = "ios"))] + { + let _ = (x, y); + SyncReturn(false) + } +} + +/// Clip cursor to a rectangle (for pointer lock). +/// +/// When `enable` is true, the cursor is clipped to the rectangle defined by +/// `left`, `top`, `right`, `bottom`. When `enable` is false, the rectangle +/// values are ignored and the cursor is unclipped. +/// +/// # Returns +/// - `true`: operation succeeded or no-op completed +/// - `false`: operation failed +/// +/// # Platform behavior +/// - Windows: uses ClipCursor API to confine cursor to the specified rectangle +/// - macOS: uses CGAssociateMouseAndMouseCursorPosition for pointer lock effect; +/// the rect coordinates are ignored (only Some/None matters) +/// - Linux: no-op, always returns `true`; use pointer warping for similar effect +/// - Android/iOS: no-op, always returns `false` +pub fn main_clip_cursor( + left: i32, + top: i32, + right: i32, + bottom: i32, + enable: bool, +) -> SyncReturn { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + let rect = if enable { + Some((left, top, right, bottom)) + } else { + None + }; + SyncReturn(crate::clip_cursor(rect)) + } + #[cfg(any(target_os = "android", target_os = "ios"))] + { + let _ = (left, top, right, bottom, enable); + SyncReturn(false) + } +} + pub fn main_get_my_id() -> String { get_id() } @@ -1748,8 +1808,99 @@ pub fn session_send_pointer(session_id: SessionID, msg: String) { super::flutter::session_send_pointer(session_id, msg); } +/// Send mouse event from Flutter to the remote peer. +/// +/// # Relative Mouse Mode Message Contract +/// +/// When the message contains a `relative_mouse_mode` field, this function validates +/// and filters activation/deactivation markers. +/// +/// **Mode Authority:** +/// The Flutter InputModel is authoritative for relative mouse mode activation/deactivation. +/// The server (via `input_service.rs`) only consumes forwarded delta movements and tracks +/// relative movement processing state, but does NOT control mode activation/deactivation. +/// +/// **Deactivation Markers are Local-Only:** +/// Deactivation markers (`relative_mouse_mode: "0"`) are NEVER forwarded to the server. +/// They are handled entirely on the client side to reset local UI state (cursor visibility, +/// pointer lock, etc.). The server does not rely on deactivation markers and should not +/// expect to receive them. +/// +/// **Contract (Flutter side MUST adhere to):** +/// 1. `relative_mouse_mode` field is ONLY present on activation/deactivation marker messages, +/// NEVER on normal pointer events (move, button, scroll). +/// 2. Deactivation marker: `{"relative_mouse_mode": "0"}` - local-only, never forwarded. +/// 3. Activation marker: `{"relative_mouse_mode": "1", "type": "move_relative", "x": "0", "y": "0"}` +/// - MUST use `type="move_relative"` with `x="0"` and `y="0"` (safe no-op). +/// - Any other combination is dropped to prevent accidental cursor movement. +/// +/// If these assumptions are violated (e.g., `relative_mouse_mode` is added to normal events), +/// legitimate mouse events may be silently dropped by the early-return logic below. pub fn session_send_mouse(session_id: SessionID, msg: String) { if let Ok(m) = serde_json::from_str::>(&msg) { + // Relative mouse mode marker validation (Flutter-only). + // This only validates and filters markers; the server tracks per-connection + // relative-movement processing state but not mode activation/deactivation. + // See doc comment above for the message contract. + if let Some(v) = m.get("relative_mouse_mode") { + let active = matches!(v.as_str(), "1" | "Y" | "on"); + + // Disable marker: local-only, never forwarded to the server. + // The server does not track mode deactivation; it simply stops receiving + // relative move events when the client exits relative mouse mode. + if !active { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + crate::keyboard::set_relative_mouse_mode_state(false); + return; + } + + // Enable marker: validate BEFORE setting state to avoid desync. + // This ensures we only mark as active if the marker will actually be forwarded. + + // Enable marker is allowed to go through only if it's a safe no-op relative move. + // This avoids accidentally moving the remote cursor (e.g. if type/x/y are missing). + let msg_type = m.get("type").map(|t| t.as_str()); + if msg_type != Some("move_relative") { + log::warn!( + "relative_mouse_mode activation marker has invalid type: {:?}, expected 'move_relative'. Dropping.", + msg_type + ); + return; + } + let x_marker = m + .get("x") + .map(|x| x.parse::().unwrap_or(0)) + .unwrap_or(0); + let y_marker = m + .get("y") + .map(|y| y.parse::().unwrap_or(0)) + .unwrap_or(0); + if x_marker != 0 || y_marker != 0 { + log::warn!( + "relative_mouse_mode activation marker has non-zero coordinates: x={}, y={}. Dropping.", + x_marker, y_marker + ); + return; + } + + // Guard against unexpected fields that could turn this no-op into a real event. + if m.contains_key("buttons") + || m.contains_key("alt") + || m.contains_key("ctrl") + || m.contains_key("shift") + || m.contains_key("command") + { + log::warn!( + "relative_mouse_mode activation marker contains unexpected fields (buttons/alt/ctrl/shift/command). Dropping." + ); + return; + } + + // All validation passed - marker will be forwarded as a no-op relative move. + #[cfg(not(any(target_os = "android", target_os = "ios")))] + crate::keyboard::set_relative_mouse_mode_state(true); + } + let alt = m.get("alt").is_some(); let ctrl = m.get("ctrl").is_some(); let shift = m.get("shift").is_some(); @@ -1769,6 +1920,7 @@ pub fn session_send_mouse(session_id: SessionID, msg: String) { "up" => MOUSE_TYPE_UP, "wheel" => MOUSE_TYPE_WHEEL, "trackpad" => MOUSE_TYPE_TRACKPAD, + "move_relative" => MOUSE_TYPE_MOVE_RELATIVE, _ => 0, }; } diff --git a/src/keyboard.rs b/src/keyboard.rs index 0497459a8..c5d4dfde8 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -32,9 +32,33 @@ const OS_LOWER_MACOS: &str = "macos"; #[allow(dead_code)] const OS_LOWER_ANDROID: &str = "android"; -#[cfg(any(target_os = "windows", target_os = "macos"))] +#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); +// Track key down state for relative mouse mode exit shortcut. +// macOS: Cmd+G (track G key) +// Windows/Linux: Ctrl+Alt (track whichever modifier was pressed last) +// This prevents the exit from retriggering on OS key-repeat. +#[cfg(all(feature = "flutter", any(target_os = "windows", target_os = "macos", target_os = "linux")))] +static EXIT_SHORTCUT_KEY_DOWN: AtomicBool = AtomicBool::new(false); + +// Track whether relative mouse mode is currently active. +// This is set by Flutter via set_relative_mouse_mode_state() and checked +// by the rdev grab loop to determine if exit shortcuts should be processed. +#[cfg(all(feature = "flutter", any(target_os = "windows", target_os = "macos", target_os = "linux")))] +static RELATIVE_MOUSE_MODE_ACTIVE: AtomicBool = AtomicBool::new(false); + +/// Set the relative mouse mode state from Flutter. +/// This is called when entering or exiting relative mouse mode. +#[cfg(all(feature = "flutter", any(target_os = "windows", target_os = "macos", target_os = "linux")))] +pub fn set_relative_mouse_mode_state(active: bool) { + RELATIVE_MOUSE_MODE_ACTIVE.store(active, Ordering::SeqCst); + // Reset exit shortcut state when mode changes to avoid stale state + if !active { + EXIT_SHORTCUT_KEY_DOWN.store(false, Ordering::SeqCst); + } +} + #[cfg(feature = "flutter")] #[cfg(not(any(target_os = "android", target_os = "ios")))] static IS_RDEV_ENABLED: AtomicBool = AtomicBool::new(false); @@ -82,7 +106,7 @@ pub mod client { GrabState::Run => { #[cfg(windows)] update_grab_get_key_name(keyboard_mode); - #[cfg(any(target_os = "windows", target_os = "macos"))] + #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] KEYBOARD_HOOKED.swap(true, Ordering::SeqCst); #[cfg(target_os = "linux")] @@ -94,7 +118,7 @@ pub mod client { release_remote_keys(keyboard_mode); - #[cfg(any(target_os = "windows", target_os = "macos"))] + #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] KEYBOARD_HOOKED.swap(false, Ordering::SeqCst); #[cfg(target_os = "linux")] @@ -266,6 +290,136 @@ fn get_keyboard_mode() -> String { "legacy".to_string() } +/// Check if exit shortcut for relative mouse mode is active. +/// Exit shortcuts (only exits, not toggles): +/// - macOS: Cmd+G +/// - Windows/Linux: Ctrl+Alt (triggered when both are pressed) +/// Note: This shortcut is only available in Flutter client. Sciter client does not support relative mouse mode. +#[cfg(feature = "flutter")] +#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] +fn is_exit_relative_mouse_shortcut(key: Key) -> bool { + let modifiers = MODIFIERS_STATE.lock().unwrap(); + + #[cfg(target_os = "macos")] + { + // macOS: Cmd+G to exit + if key != Key::KeyG { + return false; + } + let meta = *modifiers.get(&Key::MetaLeft).unwrap_or(&false) + || *modifiers.get(&Key::MetaRight).unwrap_or(&false); + return meta; + } + + #[cfg(not(target_os = "macos"))] + { + // Windows/Linux: Ctrl+Alt to exit + // Triggered when Ctrl is pressed while Alt is down, or Alt is pressed while Ctrl is down + let is_ctrl_key = key == Key::ControlLeft || key == Key::ControlRight; + let is_alt_key = key == Key::Alt || key == Key::AltGr; + + if !is_ctrl_key && !is_alt_key { + return false; + } + + let ctrl = *modifiers.get(&Key::ControlLeft).unwrap_or(&false) + || *modifiers.get(&Key::ControlRight).unwrap_or(&false); + let alt = *modifiers.get(&Key::Alt).unwrap_or(&false) + || *modifiers.get(&Key::AltGr).unwrap_or(&false); + + // When Ctrl is pressed and Alt is already down, or vice versa + (is_ctrl_key && alt) || (is_alt_key && ctrl) + } +} + +/// Notify Flutter to exit relative mouse mode. +/// Note: This is Flutter-only. Sciter client does not support relative mouse mode. +#[cfg(feature = "flutter")] +#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] +fn notify_exit_relative_mouse_mode() { + let session_id = flutter::get_cur_session_id(); + flutter::push_session_event(&session_id, "exit_relative_mouse_mode", vec![]); +} + + +/// Handle relative mouse mode shortcuts in the rdev grab loop. +/// Returns true if the event should be blocked from being sent to the peer. +#[cfg(feature = "flutter")] +#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] +#[inline] +fn can_exit_relative_mouse_mode_from_grab_loop() -> bool { + // Only process exit shortcuts when relative mouse mode is actually active. + // This prevents blocking Ctrl+Alt (or Cmd+G) when not in relative mouse mode. + if !RELATIVE_MOUSE_MODE_ACTIVE.load(Ordering::SeqCst) { + return false; + } + + let Some(session) = flutter::get_cur_session() else { + return false; + }; + + // Only for remote desktop sessions. + if !session.is_default() { + return false; + } + + // Must have keyboard permission and not be in view-only mode. + if !*session.server_keyboard_enabled.read().unwrap() { + return false; + } + let lc = session.lc.read().unwrap(); + if lc.view_only.v { + return false; + } + + // Peer must support relative mouse mode. + crate::common::is_support_relative_mouse_mode_num(lc.version) +} + +#[cfg(feature = "flutter")] +#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] +#[inline] +fn should_block_relative_mouse_shortcut(key: Key, is_press: bool) -> bool { + if !KEYBOARD_HOOKED.load(Ordering::SeqCst) { + return false; + } + + // Determine which key to track for key-up blocking based on platform + #[cfg(target_os = "macos")] + let is_tracked_key = key == Key::KeyG; + #[cfg(not(target_os = "macos"))] + let is_tracked_key = key == Key::ControlLeft + || key == Key::ControlRight + || key == Key::Alt + || key == Key::AltGr; + + // Block key up if key down was blocked (to avoid orphan key up event on remote). + // This must be checked before clearing the flag below. + if is_tracked_key && !is_press && EXIT_SHORTCUT_KEY_DOWN.swap(false, Ordering::SeqCst) { + return true; + } + + // Exit relative mouse mode shortcuts: + // - macOS: Cmd+G + // - Windows/Linux: Ctrl+Alt + // Guard it to supported/eligible sessions to avoid blocking the chord unexpectedly. + if is_exit_relative_mouse_shortcut(key) { + if !can_exit_relative_mouse_mode_from_grab_loop() { + return false; + } + if is_press { + // Only trigger exit on transition from "not pressed" to "pressed". + // This prevents retriggering on OS key-repeat. + if !EXIT_SHORTCUT_KEY_DOWN.swap(true, Ordering::SeqCst) { + notify_exit_relative_mouse_mode(); + } + } + return true; + } + + false +} + fn start_grab_loop() { std::env::set_var("KEYBOARD_ONLY", "y"); #[cfg(any(target_os = "windows", target_os = "macos"))] @@ -278,6 +432,12 @@ fn start_grab_loop() { let _scan_code = event.position_code; let _code = event.platform_code as KeyCode; + + #[cfg(feature = "flutter")] + if should_block_relative_mouse_shortcut(key, is_press) { + return None; + } + let res = if KEYBOARD_HOOKED.load(Ordering::SeqCst) { client::process_event(&get_keyboard_mode(), &event, None); if is_press { @@ -337,9 +497,14 @@ fn start_grab_loop() { #[cfg(target_os = "linux")] if let Err(err) = rdev::start_grab_listen(move |event: Event| match event.event_type { EventType::KeyPress(key) | EventType::KeyRelease(key) => { + let is_press = matches!(event.event_type, EventType::KeyPress(_)); if let Key::Unknown(keycode) = key { log::error!("rdev get unknown key, keycode is {:?}", keycode); } else { + #[cfg(feature = "flutter")] + if should_block_relative_mouse_shortcut(key, is_press) { + return None; + } client::process_event(&get_keyboard_mode(), &event, None); } None diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 93ba2987e..0a9b4f60a 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", "أدخل الملاحظة هنا"), ("note-at-conn-end-tip", "سيتم عرض هذه الملاحظة عند نهاية الاتصال"), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 03e833701..52cb7a683 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", ""), ("note-at-conn-end-tip", ""), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index d88f3745f..04c3fadd8 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", ""), ("note-at-conn-end-tip", ""), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 60ccbcbd8..1b7a5d38d 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", ""), ("note-at-conn-end-tip", ""), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index a125a9f41..f710bbc86 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", "输入备注"), ("note-at-conn-end-tip", "在连接结束时请求备注"), ("Show terminal extra keys", "显示终端扩展键"), + ("Relative mouse mode", "相对鼠标模式"), + ("rel-mouse-not-supported-peer-tip", "被控端不支持相对鼠标模式"), + ("rel-mouse-not-ready-tip", "相对鼠标模式尚未准备好,请稍后再试"), + ("rel-mouse-lock-failed-tip", "无法锁定鼠标,相对鼠标模式已禁用"), + ("rel-mouse-exit-{}-tip", "按下 {} 退出"), + ("rel-mouse-permission-lost-tip", "键盘权限被撤销。相对鼠标模式已被禁用。"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 7600f5f54..bfcf1a94f 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", ""), ("note-at-conn-end-tip", ""), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 2898629fe..48008bc51 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", ""), ("note-at-conn-end-tip", ""), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 897eb88a1..1efa68150 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", "Hier eine Notiz eingeben"), ("note-at-conn-end-tip", "Am Ende der Verbindung um eine Notiz bitten."), ("Show terminal extra keys", "Zusätzliche Tasten des Terminals anzeigen"), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index fb51a8001..d10b3fed4 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", ""), ("note-at-conn-end-tip", ""), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index f94fc49d4..60cb7b123 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -262,5 +262,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("disable-udp-tip", "Controls whether to use TCP only.\nWhen this option enabled, RustDesk will not use UDP 21116 any more, TCP 21116 will be used instead."), ("server-oss-not-support-tip", "NOTE: RustDesk server OSS doesn't include this feature."), ("note-at-conn-end-tip", "Ask for note at end of connection"), + ("rel-mouse-not-supported-peer-tip", "Relative Mouse Mode is not supported by the connected peer."), + ("rel-mouse-not-ready-tip", "Relative Mouse Mode is not ready yet. Please try again."), + ("rel-mouse-lock-failed-tip", "Failed to lock cursor. Relative Mouse Mode has been disabled."), + ("rel-mouse-exit-{}-tip", "Press {} to exit."), + ("rel-mouse-permission-lost-tip", "Keyboard permission was revoked. Relative Mouse Mode has been disabled."), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index bc9fedfb9..31026afe1 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", ""), ("note-at-conn-end-tip", ""), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 7a402cd9a..008b60ba0 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", ""), ("note-at-conn-end-tip", ""), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index 0dbfde469..6ce75fee6 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", ""), ("note-at-conn-end-tip", ""), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index f7f7b02ca..abeb81805 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", ""), ("note-at-conn-end-tip", ""), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 1bca741d7..6cfac9f4a 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", "یادداشت را اینجا وارد کنید"), ("note-at-conn-end-tip", "در پایان اتصال، یادداشت بخواهید"), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fi.rs b/src/lang/fi.rs index e97263258..f79fd9208 100644 --- a/src/lang/fi.rs +++ b/src/lang/fi.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", ""), ("note-at-conn-end-tip", ""), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 85815893e..c64ffb918 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", "saisir la note ici"), ("note-at-conn-end-tip", "Proposer de rédiger une note une fois la connexion terminée"), ("Show terminal extra keys", "Afficher les touches supplémentaires du terminal"), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ge.rs b/src/lang/ge.rs index c104a3a34..d9ec41195 100644 --- a/src/lang/ge.rs +++ b/src/lang/ge.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", ""), ("note-at-conn-end-tip", ""), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 39a3742c2..0b0a775d2 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", ""), ("note-at-conn-end-tip", ""), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index d030f482d..24b0b0b80 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", ""), ("note-at-conn-end-tip", ""), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index b3777e58d..d2cd48dff 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", "Megjegyzés bevitele"), ("note-at-conn-end-tip", "Megjegyzés a kapcsolat végén"), ("Show terminal extra keys", "További terminálgombok megjelenítése"), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index ce2b34a6e..091ea996f 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", ""), ("note-at-conn-end-tip", ""), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index b5700bf05..2f4ee009c 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", "Inserisci nota qui"), ("note-at-conn-end-tip", "Visualizza nota alla fine della connessione"), ("Show terminal extra keys", "Visualizza tasti aggiuntivi terminale"), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 9a9b08ec2..2cc68c4ec 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", "ここにメモを入力"), ("note-at-conn-end-tip", "接続終了時にメモを要求する"), ("Show terminal extra keys", "ターミナルの追加キーを表示する"), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 8ffdeefa1..77833d713 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", "여기에 노트 입력"), ("note-at-conn-end-tip", "연결이 끝날 때 메모 요청"), ("Show terminal extra keys", "터미널 추가 키 표시"), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index e3eb5b44b..f32d56fb0 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", ""), ("note-at-conn-end-tip", ""), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index a821391cf..1db3f6286 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", ""), ("note-at-conn-end-tip", ""), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 79b26c243..20872d7e1 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", ""), ("note-at-conn-end-tip", ""), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 7c06d7699..690cbfb8c 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", ""), ("note-at-conn-end-tip", ""), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index cafdc74a0..142e4f972 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", "voeg hier een opmerking toe"), ("note-at-conn-end-tip", "Vraag om een opmerking aan het einde van de verbinding"), ("Show terminal extra keys", "Toon extra toetsen voor terminal"), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 1e4af5aa9..b06a92fc2 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", "Wstaw tutaj notatkę"), ("note-at-conn-end-tip", "Poproś o notatkę po zakończeniu połączenia."), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 29ff24b89..1e489cd43 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", ""), ("note-at-conn-end-tip", ""), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index a4715b47f..8cf598b36 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", ""), ("note-at-conn-end-tip", ""), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index efbe758ef..cd8b0f929 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", ""), ("note-at-conn-end-tip", ""), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index ad9c84989..877e87a4f 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", "введите заметку"), ("note-at-conn-end-tip", "Запрашивать заметку в конце соединения"), ("Show terminal extra keys", "Показывать дополнительные кнопки терминала"), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index 19b599d5e..156391842 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", ""), ("note-at-conn-end-tip", ""), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index eafe3f244..872603a63 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", ""), ("note-at-conn-end-tip", ""), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index eb9102ac7..276d042cc 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", ""), ("note-at-conn-end-tip", ""), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 734bca256..94dc602ec 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", ""), ("note-at-conn-end-tip", ""), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index fb91966ec..1b180eb7e 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", ""), ("note-at-conn-end-tip", ""), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 773f74e62..914e937be 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", ""), ("note-at-conn-end-tip", ""), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs index bb6ef6f35..48e8fb575 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", ""), ("note-at-conn-end-tip", ""), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 3eda9e83e..bd6bbfbdd 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", ""), ("note-at-conn-end-tip", ""), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 932970d3f..5b8d1eb86 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", ""), ("note-at-conn-end-tip", ""), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 1ab02da5b..24b735243 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", "Notu buraya girin"), ("note-at-conn-end-tip", "Bağlantı bittiğinde not sorulsun"), ("Show terminal extra keys", "Terminal ek tuşlarını göster"), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 55b7c89b3..36a111960 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", "輸入備註"), ("note-at-conn-end-tip", "在連接結束時請求備註"), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 70108e8b6..dc695e0b9 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", ""), ("note-at-conn-end-tip", ""), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vi.rs b/src/lang/vi.rs index 090501015..f00a7ec77 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -730,5 +730,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", ""), ("note-at-conn-end-tip", ""), ("Show terminal extra keys", ""), + ("Relative mouse mode", ""), + ("rel-mouse-not-supported-peer-tip", ""), + ("rel-mouse-not-ready-tip", ""), + ("rel-mouse-lock-failed-tip", ""), + ("rel-mouse-exit-{}-tip", ""), + ("rel-mouse-permission-lost-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lib.rs b/src/lib.rs index 1f5061015..5621d5e2a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,8 @@ mod keyboard; pub mod platform; #[cfg(not(any(target_os = "android", target_os = "ios")))] pub use platform::{ - get_cursor, get_cursor_data, get_cursor_pos, get_focused_display, start_os_service, + clip_cursor, get_cursor, get_cursor_data, get_cursor_pos, get_focused_display, + set_cursor_pos, start_os_service, }; #[cfg(not(any(target_os = "ios")))] /// cbindgen:ignore diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 5e608aa08..c546673eb 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -97,6 +97,7 @@ extern "C" { y: *mut c_int, screen_num: *mut c_int, ) -> c_int; + fn xdo_move_mouse(xdo: Xdo, x: c_int, y: c_int, screen: c_int) -> c_int; fn xdo_new(display: *const c_char) -> Xdo; fn xdo_get_active_window(xdo: Xdo, window: *mut *mut c_void) -> c_int; fn xdo_get_window_location( @@ -174,6 +175,56 @@ pub fn get_cursor_pos() -> Option<(i32, i32)> { res } +pub fn set_cursor_pos(x: i32, y: i32) -> bool { + let mut res = false; + XDO.with(|xdo| { + match xdo.try_borrow_mut() { + Ok(xdo) => { + if xdo.is_null() { + log::debug!("set_cursor_pos: xdo is null"); + return; + } + unsafe { + let ret = xdo_move_mouse(*xdo, x, y, 0); + if ret != 0 { + log::debug!( + "set_cursor_pos: xdo_move_mouse failed with code {} for coordinates ({}, {})", + ret, x, y + ); + } + res = ret == 0; + } + } + Err(_) => { + log::debug!("set_cursor_pos: failed to borrow xdo"); + } + } + }); + res +} + +/// Clip cursor - Linux implementation is a no-op. +/// +/// On X11, there's no direct equivalent to Windows ClipCursor. XGrabPointer +/// can confine the pointer but requires a window handle and has side effects. +/// +/// On Wayland, pointer constraints require the zwp_pointer_constraints_v1 +/// protocol which is compositor-dependent. +/// +/// For relative mouse mode on Linux, the Flutter side uses pointer warping +/// (set_cursor_pos) to re-center the cursor after each movement, which achieves +/// a similar effect without requiring cursor clipping. +/// +/// Returns true (always succeeds as no-op). +pub fn clip_cursor(_rect: Option<(i32, i32, i32, i32)>) -> bool { + // Log only once per process to avoid flooding logs when called frequently. + static LOGGED: AtomicBool = AtomicBool::new(false); + if !LOGGED.swap(true, Ordering::Relaxed) { + log::debug!("clip_cursor called (no-op on Linux, this message is logged only once)"); + } + true +} + pub fn reset_input_cache() {} pub fn get_focused_display(displays: Vec) -> Option { diff --git a/src/platform/macos.rs b/src/platform/macos.rs index bc13260a5..b923c6c17 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -32,8 +32,12 @@ use std::{ os::unix::process::CommandExt, path::{Path, PathBuf}, process::{Command, Stdio}, + sync::Mutex, }; +// macOS boolean_t is defined as `int` in +type BooleanT = hbb_common::libc::c_int; + static PRIVILEGES_SCRIPTS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/platform/privileges_scripts"); static mut LATEST_SEED: i32 = 0; @@ -42,6 +46,11 @@ static mut LATEST_SEED: i32 = 0; // using one that includes the custom client name. const UPDATE_TEMP_DIR: &str = "/tmp/.rustdeskupdate"; +/// Global mutex to serialize CoreGraphics cursor operations. +/// This prevents race conditions between cursor visibility (hide depth tracking) +/// and cursor positioning/clipping operations. +static CG_CURSOR_MUTEX: Mutex<()> = Mutex::new(()); + extern "C" { fn CGSCurrentCursorSeed() -> i32; fn CGEventCreate(r: *const c_void) -> *const c_void; @@ -64,6 +73,8 @@ extern "C" { fn majorVersion() -> u32; fn MacGetMode(display: u32, width: *mut u32, height: *mut u32) -> BOOL; fn MacSetMode(display: u32, width: u32, height: u32, tryHiDPI: bool) -> BOOL; + fn CGWarpMouseCursorPosition(newCursorPosition: CGPoint) -> CGError; + fn CGAssociateMouseAndMouseCursorPosition(connected: BooleanT) -> CGError; } pub fn major_version() -> u32 { @@ -387,6 +398,99 @@ pub fn get_cursor_pos() -> Option<(i32, i32)> { */ } +/// Warp the mouse cursor to the specified screen position. +/// +/// # Thread Safety +/// This function affects global cursor state and acquires `CG_CURSOR_MUTEX`. +/// Callers must ensure no nested calls occur while the mutex is held. +/// +/// # Arguments +/// * `x` - X coordinate in screen points (macOS uses points, not pixels) +/// * `y` - Y coordinate in screen points +pub fn set_cursor_pos(x: i32, y: i32) -> bool { + // Acquire lock with deadlock detection in debug builds. + // In debug builds, try_lock detects re-entrant calls early; on failure we return immediately. + // In release builds, we use blocking lock() which will wait if contended. + #[cfg(debug_assertions)] + let _guard = match CG_CURSOR_MUTEX.try_lock() { + Ok(guard) => guard, + Err(std::sync::TryLockError::WouldBlock) => { + log::error!("[BUG] set_cursor_pos: CG_CURSOR_MUTEX is already held - potential deadlock!"); + debug_assert!(false, "Re-entrant call to set_cursor_pos detected"); + return false; + } + Err(std::sync::TryLockError::Poisoned(e)) => e.into_inner(), + }; + #[cfg(not(debug_assertions))] + let _guard = CG_CURSOR_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + unsafe { + let result = CGWarpMouseCursorPosition(CGPoint { + x: x as f64, + y: y as f64, + }); + if result != CGError::Success { + log::error!( + "CGWarpMouseCursorPosition({}, {}) returned error: {:?}", + x, + y, + result + ); + } + result == CGError::Success + } +} + +/// Toggle pointer lock (dissociate/associate mouse from cursor position). +/// +/// On macOS, cursor clipping is not supported directly like Windows ClipCursor. +/// Instead, we use CGAssociateMouseAndMouseCursorPosition to dissociate mouse +/// movement from cursor position, achieving a "pointer lock" effect. +/// +/// # Thread Safety +/// This function affects global cursor state and acquires `CG_CURSOR_MUTEX`. +/// Callers must ensure only one owner toggles pointer lock at a time; +/// nested Some/None transitions from different call sites may cause unexpected behavior. +/// +/// # Arguments +/// * `rect` - When `Some(_)`, dissociates mouse from cursor (enables pointer lock). +/// When `None`, re-associates mouse with cursor (disables pointer lock). +/// The rect coordinate values are ignored on macOS; only `Some`/`None` matters. +/// The parameter signature matches Windows for API consistency. +pub fn clip_cursor(rect: Option<(i32, i32, i32, i32)>) -> bool { + // Acquire lock with deadlock detection in debug builds. + // In debug builds, try_lock detects re-entrant calls early; on failure we return immediately. + // In release builds, we use blocking lock() which will wait if contended. + #[cfg(debug_assertions)] + let _guard = match CG_CURSOR_MUTEX.try_lock() { + Ok(guard) => guard, + Err(std::sync::TryLockError::WouldBlock) => { + log::error!("[BUG] clip_cursor: CG_CURSOR_MUTEX is already held - potential deadlock!"); + debug_assert!(false, "Re-entrant call to clip_cursor detected"); + return false; + } + Err(std::sync::TryLockError::Poisoned(e)) => e.into_inner(), + }; + #[cfg(not(debug_assertions))] + let _guard = CG_CURSOR_MUTEX.lock().unwrap_or_else(|e| e.into_inner()); + // CGAssociateMouseAndMouseCursorPosition takes a boolean_t: + // 1 (true) = associate mouse with cursor position (normal mode) + // 0 (false) = dissociate mouse from cursor position (pointer lock mode) + // When rect is Some, we want pointer lock (dissociate), so associate = false (0). + // When rect is None, we want normal mode (associate), so associate = true (1). + let associate: BooleanT = if rect.is_some() { 0 } else { 1 }; + unsafe { + let result = CGAssociateMouseAndMouseCursorPosition(associate); + if result != CGError::Success { + log::warn!( + "CGAssociateMouseAndMouseCursorPosition({}) returned error: {:?}", + associate, + result + ); + } + result == CGError::Success + } +} + pub fn get_focused_display(displays: Vec) -> Option { autoreleasepool(|| unsafe_get_focused_display(displays)) } diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 34700e614..c1bc38232 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -26,18 +26,13 @@ pub mod linux_desktop_manager; #[cfg(target_os = "linux")] pub mod gtk_sudo; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use hbb_common::{ - message_proto::CursorData, - sysinfo::Pid, - ResultType, -}; #[cfg(all( not(all(target_os = "windows", not(target_pointer_width = "64"))), - not(any(target_os = "android", target_os = "ios"))))] -use hbb_common::{ - sysinfo::System, -}; + not(any(target_os = "android", target_os = "ios")) +))] +use hbb_common::sysinfo::System; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use hbb_common::{message_proto::CursorData, sysinfo::Pid, ResultType}; use std::sync::{Arc, Mutex}; #[cfg(not(any(target_os = "macos", target_os = "android", target_os = "ios")))] pub const SERVICE_INTERVAL: u64 = 300; diff --git a/src/platform/windows.rs b/src/platform/windows.rs index bddeb4302..c40e87441 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -116,12 +116,51 @@ pub fn get_focused_display(displays: Vec) -> Option { pub fn get_cursor_pos() -> Option<(i32, i32)> { unsafe { - #[allow(invalid_value)] - let mut out = mem::MaybeUninit::uninit().assume_init(); - if GetCursorPos(&mut out) == FALSE { + let mut out = mem::MaybeUninit::::uninit(); + if GetCursorPos(out.as_mut_ptr()) == FALSE { return None; } - return Some((out.x, out.y)); + let out = out.assume_init(); + Some((out.x, out.y)) + } +} + +pub fn set_cursor_pos(x: i32, y: i32) -> bool { + unsafe { + if SetCursorPos(x, y) == FALSE { + let err = GetLastError(); + log::warn!("SetCursorPos failed: x={}, y={}, error_code={}", x, y, err); + return false; + } + true + } +} + +/// Clip cursor to a rectangle. Pass None to unclip. +pub fn clip_cursor(rect: Option<(i32, i32, i32, i32)>) -> bool { + unsafe { + let result = match rect { + Some((left, top, right, bottom)) => { + let r = RECT { + left, + top, + right, + bottom, + }; + ClipCursor(&r) + } + None => ClipCursor(std::ptr::null()), + }; + if result == FALSE { + let err = GetLastError(); + log::warn!( + "ClipCursor failed: rect={:?}, error_code={}", + rect, + err + ); + return false; + } + true } } diff --git a/src/server/connection.rs b/src/server/connection.rs index 1e7758887..d28373459 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -5173,9 +5173,13 @@ impl Retina { #[inline] fn on_mouse_event(&mut self, e: &mut MouseEvent, current: usize) { - let evt_type = e.mask & 0x7; - if evt_type == crate::input::MOUSE_TYPE_WHEEL { - // x and y are always 0, +1 or -1 + let evt_type = e.mask & crate::input::MOUSE_TYPE_MASK; + // Delta-based events do not contain absolute coordinates. + // Avoid applying Retina coordinate scaling to them. + if evt_type == crate::input::MOUSE_TYPE_WHEEL + || evt_type == crate::input::MOUSE_TYPE_TRACKPAD + || evt_type == crate::input::MOUSE_TYPE_MOVE_RELATIVE + { return; } let Some(d) = self.displays.get(current) else { @@ -5421,6 +5425,9 @@ mod raii { .unwrap() .on_connection_close(self.0); } + // Clear per-connection state to avoid stale behavior if conn ids are reused. + #[cfg(not(any(target_os = "android", target_os = "ios")))] + clear_relative_mouse_active(self.0); AUTHED_CONNS.lock().unwrap().retain(|c| c.conn_id != self.0); let remote_count = AUTHED_CONNS .lock() diff --git a/src/server/input_service.rs b/src/server/input_service.rs index adb6a7a97..b1c2d66b6 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -26,6 +26,7 @@ use std::{ thread, time::{self, Duration, Instant}, }; + #[cfg(windows)] use winapi::um::winuser::WHEEL_DELTA; @@ -447,7 +448,36 @@ lazy_static::lazy_static! { static ref KEYS_DOWN: Arc>> = Default::default(); static ref LATEST_PEER_INPUT_CURSOR: Arc> = Default::default(); static ref LATEST_SYS_CURSOR_POS: Arc, (i32, i32))>> = Arc::new(Mutex::new((None, (INVALID_CURSOR_POS, INVALID_CURSOR_POS)))); + // Track connections that are currently using relative mouse movement. + // Used to disable whiteboard/cursor display for all events while in relative mode. + static ref RELATIVE_MOUSE_CONNS: Arc>> = Default::default(); } + +#[inline] +fn set_relative_mouse_active(conn: i32, active: bool) { + let mut lock = RELATIVE_MOUSE_CONNS.lock().unwrap(); + if active { + lock.insert(conn); + } else { + lock.remove(&conn); + } +} + +#[inline] +fn is_relative_mouse_active(conn: i32) -> bool { + RELATIVE_MOUSE_CONNS.lock().unwrap().contains(&conn) +} + +/// Clears the relative mouse mode state for a connection. +/// +/// This must be called when an authenticated connection is dropped (during connection teardown) +/// to avoid leaking the connection id in `RELATIVE_MOUSE_CONNS` (a `Mutex>`). +/// Callers are responsible for invoking this on disconnect. +#[inline] +pub(crate) fn clear_relative_mouse_active(conn: i32) { + set_relative_mouse_active(conn, false); +} + static EXITING: AtomicBool = AtomicBool::new(false); const MOUSE_MOVE_PROTECTION_TIMEOUT: Duration = Duration::from_millis(1_000); @@ -644,8 +674,8 @@ async fn set_uinput_resolution(minx: i32, maxx: i32, miny: i32, maxy: i32) -> Re pub fn is_left_up(evt: &MouseEvent) -> bool { let buttons = evt.mask >> 3; - let evt_type = evt.mask & 0x7; - return buttons == 1 && evt_type == 2; + let evt_type = evt.mask & MOUSE_TYPE_MASK; + buttons == MOUSE_BUTTON_LEFT && evt_type == MOUSE_TYPE_UP } #[cfg(windows)] @@ -1003,8 +1033,16 @@ pub fn handle_mouse_( handle_mouse_simulation_(evt, conn); } #[cfg(not(any(target_os = "android", target_os = "ios")))] - if _show_cursor { - handle_mouse_show_cursor_(evt, conn, _username, _argb); + { + let evt_type = evt.mask & MOUSE_TYPE_MASK; + // Relative (delta) mouse events do not include absolute coordinates, so + // whiteboard/cursor rendering must be disabled during relative mode to prevent + // incorrect cursor/whiteboard updates. We check both is_relative_mouse_active(conn) + // (connection already in relative mode from prior events) and evt_type (current + // event is relative) to guard against the first relative event before the flag is set. + if _show_cursor && !is_relative_mouse_active(conn) && evt_type != MOUSE_TYPE_MOVE_RELATIVE { + handle_mouse_show_cursor_(evt, conn, _username, _argb); + } } } @@ -1020,7 +1058,7 @@ pub fn handle_mouse_simulation_(evt: &MouseEvent, conn: i32) { #[cfg(windows)] crate::platform::windows::try_change_desktop(); let buttons = evt.mask >> 3; - let evt_type = evt.mask & 0x7; + let evt_type = evt.mask & MOUSE_TYPE_MASK; let mut en = ENIGO.lock().unwrap(); #[cfg(target_os = "macos")] en.set_ignore_flags(enigo_ignore_flags()); @@ -1048,6 +1086,8 @@ pub fn handle_mouse_simulation_(evt: &MouseEvent, conn: i32) { } match evt_type { MOUSE_TYPE_MOVE => { + // Switching back to absolute movement implicitly disables relative mouse mode. + set_relative_mouse_active(conn, false); en.mouse_move_to(evt.x, evt.y); *LATEST_PEER_INPUT_CURSOR.lock().unwrap() = Input { conn, @@ -1056,6 +1096,28 @@ pub fn handle_mouse_simulation_(evt: &MouseEvent, conn: i32) { y: evt.y, }; } + // MOUSE_TYPE_MOVE_RELATIVE: Relative mouse movement for gaming/3D applications. + // Each client independently decides whether to use relative mode. + // Multiple clients can mix absolute and relative movements without conflict, + // as the server simply applies the delta to the current cursor position. + MOUSE_TYPE_MOVE_RELATIVE => { + set_relative_mouse_active(conn, true); + // Clamp delta to prevent extreme/malicious values from reaching OS APIs. + // This matches the Flutter client's kMaxRelativeMouseDelta constant. + const MAX_RELATIVE_MOUSE_DELTA: i32 = 10000; + let dx = evt.x.clamp(-MAX_RELATIVE_MOUSE_DELTA, MAX_RELATIVE_MOUSE_DELTA); + let dy = evt.y.clamp(-MAX_RELATIVE_MOUSE_DELTA, MAX_RELATIVE_MOUSE_DELTA); + en.mouse_move_relative(dx, dy); + // Get actual cursor position after relative movement for tracking + if let Some((x, y)) = crate::get_cursor_pos() { + *LATEST_PEER_INPUT_CURSOR.lock().unwrap() = Input { + conn, + time: get_time(), + x, + y, + }; + } + } MOUSE_TYPE_DOWN => match buttons { MOUSE_BUTTON_LEFT => { allow_err!(en.mouse_down(MouseButton::Left)); @@ -1154,7 +1216,7 @@ pub fn handle_mouse_simulation_(evt: &MouseEvent, conn: i32) { #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn handle_mouse_show_cursor_(evt: &MouseEvent, conn: i32, username: String, argb: u32) { let buttons = evt.mask >> 3; - let evt_type = evt.mask & 0x7; + let evt_type = evt.mask & MOUSE_TYPE_MASK; match evt_type { MOUSE_TYPE_MOVE => { whiteboard::update_whiteboard( @@ -1170,11 +1232,22 @@ pub fn handle_mouse_show_cursor_(evt: &MouseEvent, conn: i32, username: String, } MOUSE_TYPE_UP => { if buttons == MOUSE_BUTTON_LEFT { + // Some clients intentionally send button events without coordinates. + // Fall back to the last known cursor position to avoid jumping to (0, 0). + // TODO(protocol): (0, 0) is a valid screen coordinate. Consider using a dedicated + // sentinel value (e.g. INVALID_CURSOR_POS) or a protocol-level flag to distinguish + // "coordinates not provided" from "coordinates are (0, 0)". Impact is minor since + // this only affects whiteboard rendering and clicking exactly at (0, 0) is rare. + let (x, y) = if evt.x == 0 && evt.y == 0 { + get_last_input_cursor_pos() + } else { + (evt.x, evt.y) + }; whiteboard::update_whiteboard( whiteboard::get_key_cursor(conn), whiteboard::CustomEvent::Cursor(whiteboard::Cursor { - x: evt.x as _, - y: evt.y as _, + x: x as _, + y: y as _, argb, btns: buttons, text: username, diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 88ee7bc9b..9ea0cba5b 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1,6 +1,9 @@ use crate::{ common::{get_supported_keyboard_modes, is_keyboard_mode_supported}, - input::{MOUSE_BUTTON_LEFT, MOUSE_TYPE_DOWN, MOUSE_TYPE_UP, MOUSE_TYPE_WHEEL}, + input::{ + MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT, MOUSE_TYPE_DOWN, MOUSE_TYPE_MASK, + MOUSE_TYPE_TRACKPAD, MOUSE_TYPE_UP, MOUSE_TYPE_WHEEL, + }, ui_interface::use_texture_render, }; use async_trait::async_trait; @@ -1222,7 +1225,9 @@ impl Session { } } - let (x, y) = if mask == MOUSE_TYPE_WHEEL || mask == MOUSE_TYPE_TRACKPAD { + // Compute event type once using MOUSE_TYPE_MASK for reuse + let event_type = mask & MOUSE_TYPE_MASK; + let (x, y) = if event_type == MOUSE_TYPE_WHEEL || event_type == MOUSE_TYPE_TRACKPAD { self.get_scroll_xy((x, y)) } else { (x, y) @@ -1231,8 +1236,6 @@ impl Session { // #[cfg(not(any(target_os = "android", target_os = "ios")))] let (alt, ctrl, shift, command) = keyboard::client::get_modifiers_state(alt, ctrl, shift, command); - - use crate::input::*; let is_left = (mask & (MOUSE_BUTTON_LEFT << 3)) > 0; let is_right = (mask & (MOUSE_BUTTON_RIGHT << 3)) > 0; if is_left ^ is_right { @@ -1252,9 +1255,8 @@ impl Session { // to-do: how about ctrl + left from win to macos if cfg!(target_os = "macos") { let buttons = mask >> 3; - let evt_type = mask & 0x7; if buttons == MOUSE_BUTTON_LEFT - && evt_type == MOUSE_TYPE_DOWN + && event_type == MOUSE_TYPE_DOWN && ctrl && self.peer_platform() != "Mac OS" { From 98362eaca036588039d10d2c1e9a9260de7a310d Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 9 Jan 2026 15:34:51 +0800 Subject: [PATCH 069/277] add Changelog link in update help card (#13997) Signed-off-by: 21pages --- flutter/lib/desktop/pages/desktop_home_page.dart | 6 +++++- src/lang/ar.rs | 1 + src/lang/be.rs | 1 + src/lang/bg.rs | 1 + src/lang/ca.rs | 1 + src/lang/cn.rs | 1 + src/lang/cs.rs | 1 + src/lang/da.rs | 1 + src/lang/de.rs | 1 + src/lang/el.rs | 1 + src/lang/eo.rs | 1 + src/lang/es.rs | 1 + src/lang/et.rs | 1 + src/lang/eu.rs | 1 + src/lang/fa.rs | 1 + src/lang/fi.rs | 1 + src/lang/fr.rs | 1 + src/lang/ge.rs | 1 + src/lang/he.rs | 1 + src/lang/hr.rs | 1 + src/lang/hu.rs | 1 + src/lang/id.rs | 1 + src/lang/it.rs | 1 + src/lang/ja.rs | 1 + src/lang/ko.rs | 1 + src/lang/kz.rs | 1 + src/lang/lt.rs | 1 + src/lang/lv.rs | 1 + src/lang/nb.rs | 1 + src/lang/nl.rs | 1 + src/lang/pl.rs | 1 + src/lang/pt_PT.rs | 1 + src/lang/ptbr.rs | 1 + src/lang/ro.rs | 1 + src/lang/ru.rs | 1 + src/lang/sc.rs | 1 + src/lang/sk.rs | 1 + src/lang/sl.rs | 1 + src/lang/sq.rs | 1 + src/lang/sr.rs | 1 + src/lang/sv.rs | 1 + src/lang/ta.rs | 1 + src/lang/template.rs | 1 + src/lang/th.rs | 1 + src/lang/tr.rs | 1 + src/lang/tw.rs | 1 + src/lang/uk.rs | 1 + src/lang/vi.rs | 1 + 48 files changed, 52 insertions(+), 1 deletion(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 0a75175db..339ecddb0 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -450,7 +450,11 @@ class _DesktopHomePageState extends State "${translate("new-version-of-{${bind.mainGetAppNameSync()}}-tip")} (${bind.mainGetNewVersion()}).", btnText, onPressed, - closeButton: true); + closeButton: true, + help: isToUpdate ? 'Changelog' : null, + link: isToUpdate + ? 'https://github.com/rustdesk/rustdesk/releases/tag/${bind.mainGetNewVersion()}' + : null); } if (systemError.isNotEmpty) { return buildInstallCard("", systemError, "", () {}); diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 0a9b4f60a..cee43eaad 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 52cb7a683..6d090d45f 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 04c3fadd8..e7e56f22b 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 1b7a5d38d..fc75a83b9 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index f710bbc86..1f3b02577 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", "无法锁定鼠标,相对鼠标模式已禁用"), ("rel-mouse-exit-{}-tip", "按下 {} 退出"), ("rel-mouse-permission-lost-tip", "键盘权限被撤销。相对鼠标模式已被禁用。"), + ("Changelog", "更新日志"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index bfcf1a94f..ccba57553 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 48008bc51..c90fa7118 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 1efa68150..3d9568a52 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index d10b3fed4..edfa93e55 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 31026afe1..c41845731 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 008b60ba0..d6958e643 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index 6ce75fee6..f78990e99 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index abeb81805..cb1fdc143 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 6cfac9f4a..18d331007 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fi.rs b/src/lang/fi.rs index f79fd9208..00f9692c4 100644 --- a/src/lang/fi.rs +++ b/src/lang/fi.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index c64ffb918..c67db4203 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ge.rs b/src/lang/ge.rs index d9ec41195..e59fca4dd 100644 --- a/src/lang/ge.rs +++ b/src/lang/ge.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 0b0a775d2..a92905bd9 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 24b0b0b80..e998b0672 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index d2cd48dff..dccd191dc 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 091ea996f..0bd200e4b 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 2f4ee009c..94307efd4 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 2cc68c4ec..fd479c266 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 77833d713..c009f29b3 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index f32d56fb0..9f5cabc78 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 1db3f6286..0e0711d4d 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 20872d7e1..20a1abb94 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 690cbfb8c..67bfebdf7 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 142e4f972..6b2c7dc66 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index b06a92fc2..2bae03e2d 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 1e489cd43..d97013c90 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 8cf598b36..25624b87f 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index cd8b0f929..bd76b34c3 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 877e87a4f..38b737136 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index 156391842..a775bf234 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 872603a63..efbcac7ed 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 276d042cc..bf0a1e6b4 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 94dc602ec..8f1e333a4 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 1b180eb7e..407725e9b 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 914e937be..d82883dc2 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs index 48e8fb575..a4ac03d78 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index bd6bbfbdd..a0a8e31c8 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 5b8d1eb86..86b3522d3 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 24b735243..ec7633743 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 36a111960..e93ae0f15 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index dc695e0b9..7c58f7e91 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vi.rs b/src/lang/vi.rs index f00a7ec77..dbfa11da6 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -736,5 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", ""), ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), + ("Changelog", ""), ].iter().cloned().collect(); } From f3bbcc4f55a14f74174af6764e41a28ad85c7999 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sat, 10 Jan 2026 00:55:00 +0800 Subject: [PATCH 070/277] refact(sign): skip signed files (#14005) Signed-off-by: fufesou --- res/job.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/res/job.py b/res/job.py index 13ea9e81d..a76b6b5cb 100755 --- a/res/job.py +++ b/res/job.py @@ -205,6 +205,8 @@ def sign_files(dir_path, only_ext=None): if not only_ext[i].startswith("."): only_ext[i] = "." + only_ext[i] for root, dirs, files in os.walk(dir_path): + if "RustDeskPrinterDriver" in root or "usbmmidd_v2" in root: + continue for file in files: file_path = os.path.join(root, file) _, ext = os.path.splitext(file_path) From 82fcab26b1c18cd8588408c266af8f5d3599f243 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sat, 10 Jan 2026 02:01:32 +0800 Subject: [PATCH 071/277] refact(sign): skip signed files (#14006) Signed-off-by: fufesou --- res/job.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/res/job.py b/res/job.py index a76b6b5cb..e53105fd3 100755 --- a/res/job.py +++ b/res/job.py @@ -205,11 +205,13 @@ def sign_files(dir_path, only_ext=None): if not only_ext[i].startswith("."): only_ext[i] = "." + only_ext[i] for root, dirs, files in os.walk(dir_path): - if "RustDeskPrinterDriver" in root or "usbmmidd_v2" in root: - continue + is_signed_dir = "RustDeskPrinterDriver" in root or "usbmmidd_v2" in root for file in files: file_path = os.path.join(root, file) _, ext = os.path.splitext(file_path) + # only sign the exe files in signed dirs + if is_signed_dir and ext not in [".exe"]: + continue if only_ext and ext not in only_ext: continue if ext in SIGN_EXTENSIONS: From b0c12bd86b7540bf411bd6a9fd17470e76994319 Mon Sep 17 00:00:00 2001 From: Sunev Date: Sat, 10 Jan 2026 15:29:59 +0800 Subject: [PATCH 072/277] Update signing conditions for rustdesk files (#14010) Now ```env.SIGN_BASE_URL``` would never be ```''```, yet could be ```'-2'``` while ```secrets.SIGN_BASE_URL``` was undefined. --- .github/workflows/flutter-build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index d2828b819..22b24d483 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -234,7 +234,7 @@ jobs: path: rustdesk - name: Sign rustdesk files - if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '' + if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2' shell: bash run: | pip3 install requests argparse @@ -266,7 +266,7 @@ jobs: sha256sum ../../SignOutput/rustdesk-*.msi - name: Sign rustdesk self-extracted file - if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '' + if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2' shell: bash run: | BASE_URL=${{ env.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput @@ -400,7 +400,7 @@ jobs: path: Release - name: Sign rustdesk files - if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '' + if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2' shell: bash run: | pip3 install requests argparse @@ -418,7 +418,7 @@ jobs: mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.exe - name: Sign rustdesk self-extracted file - if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '' + if: env.UPLOAD_ARTIFACT == 'true' && env.SIGN_BASE_URL != '-2' shell: bash run: | BASE_URL=${{ env.SIGN_BASE_URL }} SECRET_KEY=${{ secrets.SIGN_SECRET_KEY }} python3 res/job.py sign_files ./SignOutput/ From a97997952dd3a4ce853454b1e34fc8fee4cf644e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?VenusGirl=E2=9D=A4?= Date: Mon, 12 Jan 2026 11:58:23 +0900 Subject: [PATCH 073/277] Update Korean (#13996) Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> --- src/lang/ko.rs | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/lang/ko.rs b/src/lang/ko.rs index c009f29b3..c36a4ee7e 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -3,7 +3,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "상태"), ("Your Desktop", "내 데스크탑"), - ("desk_tip", "이 ID와 비밀번호로 데스크톱에 액세스할 수 있습니다."), + ("desk_tip", "이 ID와 비밀번호로 데스크탑에 액세스할 수 있습니다."), ("Password", "비밀번호"), ("Ready", "준비 완료"), ("Established", "연결됨"), @@ -136,20 +136,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ID does not exist", "ID가 존재하지 않습니다"), ("Failed to connect to rendezvous server", "랑데부 서버 연결에 실패했습니다"), ("Please try later", "나중에 시도해 주세요"), - ("Remote desktop is offline", "원격 데스크톱이 오프라인입니다"), + ("Remote desktop is offline", "원격 데스크탑이 오프라인입니다"), ("Key mismatch", "키가 일치하지 않습니다"), ("Timeout", "시간 초과"), ("Failed to connect to relay server", "릴레이 서버 연결에 실패했습니다"), ("Failed to connect via rendezvous server", "랑데부 서버를 통한 연결에 실패했습니다"), ("Failed to connect via relay server", "릴레이 서버를 통한 연결에 실패했습니다"), - ("Failed to make direct connection to remote desktop", "원격 데스크톱에 직접 연결에 실패했습니다"), + ("Failed to make direct connection to remote desktop", "원격 데스크탑에 직접 연결에 실패했습니다"), ("Set Password", "비밀번호 설정"), ("OS Password", "OS 비밀번호"), ("install_tip", "UAC로 인해 경우에 따라 RustDesk가 원격 쪽에서 제대로 작동하지 않을 수 있습니다. UAC를 피하려면 아래 버튼을 클릭하여 시스템에 RustDesk를 설치하세요."), ("Click to upgrade", "업그레이드"), ("Configure", "구성"), - ("config_acc", "데스크톱을 원격으로 제어하려면 RustDesk에 \"접근성\" 권한을 부여해야 합니다."), - ("config_screen", "데스크톱에 원격으로 액세스하려면 RustDesk에 \"화면 녹화\" 권한을 부여해야 합니다."), + ("config_acc", "데스크탑을 원격으로 제어하려면 RustDesk에 \"접근성\" 권한을 부여해야 합니다."), + ("config_screen", "데스크탑에 원격으로 액세스하려면 RustDesk에 \"화면 녹화\" 권한을 부여해야 합니다."), ("Installing ...", "설치 중..."), ("Install", "설치하기"), ("Installation", "설치"), @@ -370,7 +370,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Write a message", "메시지 쓰기"), ("Prompt", "프롬프트"), ("Please wait for confirmation of UAC...", "UAC 확인을 기다려주세요..."), - ("elevated_foreground_window_tip", "원격 데스크톱의 현재 창을 작동하려면 더 높은 권한이 필요하므로 일시적으로 마우스와 키보드를 사용할 수 없습니다. 원격 사용자에게 현재 창을 최소화하도록 요청하거나 연결 관리 창에서 권한 상승 버튼을 클릭할 수 있습니다. 이 문제를 방지하려면 원격 장치에 소프트웨어를 설치하는 것이 좋습니다."), + ("elevated_foreground_window_tip", "원격 데스크탑의 현재 창을 작동하려면 더 높은 권한이 필요하므로 일시적으로 마우스와 키보드를 사용할 수 없습니다. 원격 사용자에게 현재 창을 최소화하도록 요청하거나 연결 관리 창에서 권한 상승 버튼을 클릭할 수 있습니다. 이 문제를 방지하려면 원격 장치에 소프트웨어를 설치하는 것이 좋습니다."), ("Disconnected", "연결 끊김"), ("Other", "기타"), ("Confirm before closing multiple tabs", "여러 탭을 닫기 전에 확인"), @@ -378,7 +378,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Full Access", "전체 액세스"), ("Screen Share", "화면 공유"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland는 Ubuntu 21.04 이상 버전이 필요합니다."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland는 상위 버전의 Linux 배포판이 필요합니다. X11 데스크톱을 사용하거나 OS를 변경하세요."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland는 상위 버전의 Linux 배포판이 필요합니다. X11 데스크탑을 사용하거나 OS를 변경하세요."), ("JumpLink", "점프 링크"), ("Please Select the screen to be shared(Operate on the peer side).", "공유할 화면을 선택하세요 (피어 측에서 작동)"), ("Show RustDesk", "RustDesk 표시"), @@ -408,7 +408,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", "로컬 키보드 유형 선택"), ("software_render_tip", "Linux에서 Nvidia 그래픽 카드를 사용 중인데 원격 창이 연결 즉시 닫히는 경우 오픈 소스 Nouveau 드라이버로 전환하고 소프트웨어 렌더링을 사용하기로 선택하는 것이 도움이 될 수 있습니다. 소프트웨어를 재시작해야 합니다."), ("Always use software rendering", "항상 소프트웨어 렌더링 사용"), - ("config_input", "키보드로 원격 데스크톱을 제어하려면 RustDesk에 \"입력 모니터링\" 권한을 부여해야 합니다."), + ("config_input", "키보드로 원격 데스크탑을 제어하려면 RustDesk에 \"입력 모니터링\" 권한을 부여해야 합니다."), ("config_microphone", "원격으로 통화하려면 RustDesk에 \"오디오 녹음\" 권한을 부여해야 합니다."), ("request_elevation_tip", "원격 측에 사람이 있는 경우 권한 상승을 요청할 수도 있습니다."), ("Wait", "대기"), @@ -468,14 +468,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("login_linux_tip", "X 데스크탑을 활성화하려면 제어되는 터미널의 Linux 계정에 로그인하세요"), ("verify_rustdesk_password_tip", "RustDesk 비밀번호 확인"), ("remember_account_tip", "이 계정 기억하기"), - ("os_account_desk_tip", "이 계정은 원격 OS에 로그인하고 헤드리스에서 데스크톱 세션을 활성화하는 데 사용됩니다."), + ("os_account_desk_tip", "이 계정은 원격 OS에 로그인하고 헤드리스에서 데스크탑 세션을 활성화하는 데 사용됩니다."), ("OS Account", "OS 계정"), ("another_user_login_title_tip", "다른 사용자가 이미 로그인했습니다"), ("another_user_login_text_tip", "연결 끊기"), ("xorg_not_found_title_tip", "Xorg를 찾을 수 없습니다"), ("xorg_not_found_text_tip", "Xorg를 설치해 주세요"), - ("no_desktop_title_tip", "사용 가능한 데스크톱 환경이 없습니다"), - ("no_desktop_text_tip", "GNOME 데스크톱을 설치해 주세요"), + ("no_desktop_title_tip", "사용 가능한 데스크탑 환경이 없습니다"), + ("no_desktop_text_tip", "GNOME 데스크탑을 설치해 주세요"), ("No need to elevate", "권한 상승이 필요없습니다"), ("System Sound", "시스템 소리"), ("Default", "기본"), @@ -572,7 +572,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("2FA code", "이중 인증 코드"), ("More", "더 많은"), ("enable-2fa-title", "이중 인증 사용함"), - ("enable-2fa-desc", "지금 인증앱을 설정해 주세요. 휴대폰이나 데스크톱에서 Authy, Microsoft 또는 Google 인증기와 같은 인증기 앱을 사용할 수 있습니다.\n\n앱으로 QR 코드를 스캔하고 앱에 표시된 코드를 입력하면 이중 인증이 가능합니다."), + ("enable-2fa-desc", "지금 인증앱을 설정해 주세요. 휴대폰이나 데스크탑에서 Authy, Microsoft 또는 Google 인증기와 같은 인증기 앱을 사용할 수 있습니다.\n\n앱으로 QR 코드를 스캔하고 앱에 표시된 코드를 입력하면 이중 인증이 가능합니다."), ("wrong-2fa-code", "코드를 확인할 수 없습니다. 코드와 현지 시간 설정이 올바른지 확인합니다"), ("enter-2fa-title", "이중 인증"), ("Email verification code must be 6 characters.", "이메일 인증 코드는 6자여야 합니다."), @@ -730,12 +730,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", "여기에 노트 입력"), ("note-at-conn-end-tip", "연결이 끝날 때 메모 요청"), ("Show terminal extra keys", "터미널 추가 키 표시"), - ("Relative mouse mode", ""), - ("rel-mouse-not-supported-peer-tip", ""), - ("rel-mouse-not-ready-tip", ""), - ("rel-mouse-lock-failed-tip", ""), - ("rel-mouse-exit-{}-tip", ""), - ("rel-mouse-permission-lost-tip", ""), + ("Relative mouse mode", "상대 마우스 모드"), + ("rel-mouse-not-supported-peer-tip", "연결된 피어에서 상대 마우스 모드를 지원하지 않습니다."), + ("rel-mouse-not-ready-tip", "상대 마우스 모드가 아직 준비되지 않았습니다. 다시 시도해 주세요."), + ("rel-mouse-lock-failed-tip", "커서 잠금에 실패했습니다. 상대 마우스 모드가 비활성화되었습니다"), + ("rel-mouse-exit-{}-tip", "종료하려면 {}을(를) 누르세요."), + ("rel-mouse-permission-lost-tip", "키보드 권한이 취소되었습니다. 상대 마우스 모드가 비활성화되었습니다."), ("Changelog", ""), ].iter().cloned().collect(); } From 5355702e9c4020659709b7b21138ff8c10109a43 Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Mon, 12 Jan 2026 03:58:42 +0100 Subject: [PATCH 074/277] Italian language update (#13998) --- src/lang/it.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 94307efd4..3785f5e0d 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -730,12 +730,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", "Inserisci nota qui"), ("note-at-conn-end-tip", "Visualizza nota alla fine della connessione"), ("Show terminal extra keys", "Visualizza tasti aggiuntivi terminale"), - ("Relative mouse mode", ""), - ("rel-mouse-not-supported-peer-tip", ""), - ("rel-mouse-not-ready-tip", ""), - ("rel-mouse-lock-failed-tip", ""), - ("rel-mouse-exit-{}-tip", ""), - ("rel-mouse-permission-lost-tip", ""), - ("Changelog", ""), + ("Relative mouse mode", "Modalità relativa mouse"), + ("rel-mouse-not-supported-peer-tip", "La modalità mouse relativa non è supportata dal peer connesso."), + ("rel-mouse-not-ready-tip", "La modalità mouse relativa non è ancora pronta. Riprova."), + ("rel-mouse-lock-failed-tip", "Impossibile bloccare il cursore. La modalità mouse relativa è stata disabilitata."), + ("rel-mouse-exit-{}-tip", "Premi {} per uscire."), + ("rel-mouse-permission-lost-tip", "È stata revocato l'accesso alla tastiera. La modalità mouse relativa è stata disabilitata."), + ("Changelog", "Novità programma"), ].iter().cloned().collect(); } From 070d4d029fb5bb04a1d7130c29650455c5346b74 Mon Sep 17 00:00:00 2001 From: Anatolij Vasilev <3026792+tolik518@users.noreply.github.com> Date: Mon, 12 Jan 2026 03:59:11 +0100 Subject: [PATCH 075/277] synchronized german translation with the current english readme (#14001) --- docs/README-DE.md | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/docs/README-DE.md b/docs/README-DE.md index c746e88d0..ba8894411 100644 --- a/docs/README-DE.md +++ b/docs/README-DE.md @@ -1,15 +1,14 @@

    - RustDesk - Your remote desktop
    - Server • + RustDesk - Dein Remote-Desktop
    KompilierenDockerDateistrukturScreenshots
    - [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Dansk] | [Ελληνικά]
    + [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Dansk] | [Ελληνικά] | [Türkçe] | [Norsk] | [Română]
    Wir brauchen Ihre Hilfe, um dieses README, die RustDesk-Benutzeroberfläche und die Dokumentation in Ihre Muttersprache zu übersetzen.

    -> [!Vorsicht] +> [!Caution] > **Haftungsausschluss bei Missbrauch::**
    > Die Entwickler von RustDesk billigen oder unterstützen keine unethische oder illegale Nutzung dieser Software. Missbrauch, wie unbefugter Zugriff, unbefugte Kontrolle oder Verletzung der Privatsphäre, verstößt strikt gegen unsere Richtlinien. Die Autoren sind nicht verantwortlich für jeglichen Missbrauch der Anwendung. @@ -28,11 +27,14 @@ RustDesk heißt jegliche Mitarbeit willkommen. Schauen Sie sich [CONTRIBUTING-DE [**Programm herunterladen**](https://github.com/rustdesk/rustdesk/releases) -[**Nächtliche Erstellung**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) +[**Nightly Builds**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) -[Get it on F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) +[Get it on Flathub](https://flathub.org/apps/com.rustdesk.RustDesk) ## Abhängigkeiten @@ -64,18 +66,19 @@ Bitte laden Sie die dynamische Bibliothek Sciter selbst herunter. ```sh sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \ libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \ - libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev + libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev ``` ### openSUSE Tumbleweed ```sh -sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel +sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel ``` + ### Fedora 28 (CentOS 8) ```sh -sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel ``` ### Arch (Manjaro) @@ -114,7 +117,7 @@ cd ```sh curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source $HOME/.cargo/env -git clone https://github.com/rustdesk/rustdesk +git clone --recurse-submodules https://github.com/rustdesk/rustdesk cd rustdesk mkdir -p target/debug wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so @@ -129,6 +132,7 @@ Beginnen Sie damit, das Repository zu klonen und den Docker-Container zu bauen: ```sh git clone https://github.com/rustdesk/rustdesk cd rustdesk +git submodule update --init --recursive docker build -t "rustdesk-builder" . ``` @@ -157,6 +161,7 @@ Bitte stellen Sie sicher, dass Sie diese Befehle im Stammverzeichnis des RustDes - **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: Video-Codec, Konfiguration, TCP/UDP-Wrapper, Protokoll-Puffer, fs-Funktionen für Dateitransfer und ein paar andere nützliche Funktionen - **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: Bildschirmaufnahme - **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: Plattformspezifische Maus- und Tastatursteuerung +- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: Datei kopieren und einfügen Implementierung für Windows, Linux, macOS. - **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI - **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: Audio/Zwischenablage/Eingabe/Videodienste und Netzwerkverbindungen - **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: Starten einer Peer-Verbindung @@ -167,10 +172,11 @@ Bitte stellen Sie sicher, dass Sie diese Befehle im Stammverzeichnis des RustDes ## Screenshots -![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) +![Verbindungsmanager](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651) -![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) +![Verbunden zu einem Windows PC](https://github.com/rustdesk/rustdesk/assets/28412477/9baa91e9-3362-4d06-aa1a-7518edcbd7ea) -![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) +![Dateiübertragung](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad) + +![TCP-Tunneling](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5) -![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) From 775b0a3c93303201c71167cfcfc9fe21fbf7b54a Mon Sep 17 00:00:00 2001 From: solokot Date: Mon, 12 Jan 2026 15:56:02 +0300 Subject: [PATCH 076/277] Update ru.rs (#14004) --- src/lang/ru.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 38b737136..ecc768a59 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -730,12 +730,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", "введите заметку"), ("note-at-conn-end-tip", "Запрашивать заметку в конце соединения"), ("Show terminal extra keys", "Показывать дополнительные кнопки терминала"), - ("Relative mouse mode", ""), - ("rel-mouse-not-supported-peer-tip", ""), - ("rel-mouse-not-ready-tip", ""), - ("rel-mouse-lock-failed-tip", ""), - ("rel-mouse-exit-{}-tip", ""), - ("rel-mouse-permission-lost-tip", ""), - ("Changelog", ""), + ("Relative mouse mode", "Режим относительного перемещения мыши"), + ("rel-mouse-not-supported-peer-tip", "Режим относительного перемещения мыши не поддерживается подключённым узлом."), + ("rel-mouse-not-ready-tip", "Режим относительного перемещения мыши ещё не готов. Попробуйте снова."), + ("rel-mouse-lock-failed-tip", "Невозможно заблокировать курсор. Режим относительного перемещения мыши отключён."), + ("rel-mouse-exit-{}-tip", "Нажмите {} для выхода."), + ("rel-mouse-permission-lost-tip", "Разрешение на использование клавиатуры отменено. Режим относительного перемещения мыши отключён."), + ("Changelog", "Журнал изменений"), ].iter().cloned().collect(); } From 21529d6ca2c4384ed9e73c2e74412b232ade484a Mon Sep 17 00:00:00 2001 From: bilimiyorum <131397022+bilimiyorum@users.noreply.github.com> Date: Mon, 12 Jan 2026 15:56:19 +0300 Subject: [PATCH 077/277] Current tr.rs (#14008) New string entries --- src/lang/tr.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lang/tr.rs b/src/lang/tr.rs index ec7633743..00b76b0c3 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -730,12 +730,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", "Notu buraya girin"), ("note-at-conn-end-tip", "Bağlantı bittiğinde not sorulsun"), ("Show terminal extra keys", "Terminal ek tuşlarını göster"), - ("Relative mouse mode", ""), - ("rel-mouse-not-supported-peer-tip", ""), - ("rel-mouse-not-ready-tip", ""), - ("rel-mouse-lock-failed-tip", ""), - ("rel-mouse-exit-{}-tip", ""), - ("rel-mouse-permission-lost-tip", ""), - ("Changelog", ""), + ("Relative mouse mode", "Fareyi göreli modda kullan"), + ("rel-mouse-not-supported-peer-tip", "Karşı taraf göreli fare modunu desteklemiyor"), + ("rel-mouse-not-ready-tip", "Göreli fare modu henüz hazır değil"), + ("rel-mouse-lock-failed-tip", "Göreli fare kilitlenemedi"), + ("rel-mouse-exit-{}-tip", "Göreli fare modundan çıkmak için {}"), + ("rel-mouse-permission-lost-tip", "Göreli fare izinleri geçerli değil"), + ("Changelog", "Değişiklik Günlüğü"), ].iter().cloned().collect(); } From e3f66973b7d67c5b9fed00f1db929a04a89b2ed2 Mon Sep 17 00:00:00 2001 From: Lynilia <89228568+Lynilia@users.noreply.github.com> Date: Mon, 12 Jan 2026 13:56:35 +0100 Subject: [PATCH 078/277] Update fr.rs (#14012) --- src/lang/fr.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lang/fr.rs b/src/lang/fr.rs index c67db4203..6a4f4b562 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -730,12 +730,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", "saisir la note ici"), ("note-at-conn-end-tip", "Proposer de rédiger une note une fois la connexion terminée"), ("Show terminal extra keys", "Afficher les touches supplémentaires du terminal"), - ("Relative mouse mode", ""), - ("rel-mouse-not-supported-peer-tip", ""), - ("rel-mouse-not-ready-tip", ""), - ("rel-mouse-lock-failed-tip", ""), - ("rel-mouse-exit-{}-tip", ""), - ("rel-mouse-permission-lost-tip", ""), - ("Changelog", ""), + ("Relative mouse mode", "Mode souris relative"), + ("rel-mouse-not-supported-peer-tip", "Le mode souris relative n’est pas pris en charge par l’appareil distant."), + ("rel-mouse-not-ready-tip", "Le mode souris relative n’est pas encore prêt ; veuillez réessayer."), + ("rel-mouse-lock-failed-tip", "Échec du verrouillage du curseur. Le mode souris relative a été désactivé."), + ("rel-mouse-exit-{}-tip", "Appuyez sur {} pour quitter."), + ("rel-mouse-permission-lost-tip", "L’autorisation de contrôle du clavier a été révoquée. Le mode souris relative a été désactivé."), + ("Changelog", "Journal des modifications"), ].iter().cloned().collect(); } From b27a93fc774f96828c0a1ed2cae08e411a6f66b7 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Mon, 12 Jan 2026 13:56:50 +0100 Subject: [PATCH 079/277] Update de.rs (#14013) --- src/lang/de.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 3d9568a52..f7521daff 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -730,12 +730,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", "Hier eine Notiz eingeben"), ("note-at-conn-end-tip", "Am Ende der Verbindung um eine Notiz bitten."), ("Show terminal extra keys", "Zusätzliche Tasten des Terminals anzeigen"), - ("Relative mouse mode", ""), - ("rel-mouse-not-supported-peer-tip", ""), - ("rel-mouse-not-ready-tip", ""), - ("rel-mouse-lock-failed-tip", ""), - ("rel-mouse-exit-{}-tip", ""), - ("rel-mouse-permission-lost-tip", ""), - ("Changelog", ""), + ("Relative mouse mode", "Relativer Mausmodus"), + ("rel-mouse-not-supported-peer-tip", "Der relative Mausmodus wird von der verbundenen Gegenstelle nicht unterstützt."), + ("rel-mouse-not-ready-tip", "Der relative Mausmodus ist noch nicht bereit. Bitte versuchen Sie es erneut."), + ("rel-mouse-lock-failed-tip", "Cursor konnte nicht gesperrt werden. Der relative Mausmodus wurde deaktiviert."), + ("rel-mouse-exit-{}-tip", "Drücken Sie {} zum Beenden."), + ("rel-mouse-permission-lost-tip", "Die Tastaturberechtigung wurde widerrufen. Der relative Mausmodus wurde deaktiviert."), + ("Changelog", "Änderungsprotokoll"), ].iter().cloned().collect(); } From dab9ed711c3fde00317045a4e46f376f149a435d Mon Sep 17 00:00:00 2001 From: John Fowler Date: Mon, 12 Jan 2026 13:57:05 +0100 Subject: [PATCH 080/277] Update Hungarian translations in hu.rs (#14014) Translation of new strings and some fixes. John Fowler. --- src/lang/hu.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lang/hu.rs b/src/lang/hu.rs index dccd191dc..d9300bae6 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -727,15 +727,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Disable UDP", "UDP letiltása"), ("disable-udp-tip", "Meghatározza, hogy csak TCP-t használjon-e. Ha ez az beállítás engedélyezve van, a RustDesk nem fogja többé használni a 21116-os UDP-portot, helyette a 21116-os TCP-portot fogja használni."), ("server-oss-not-support-tip", "MEGJEGYZÉS: Az OSS RustDesk kiszolgáló nem támogatja ezt a funkciót."), - ("input note here", "Megjegyzés bevitele"), - ("note-at-conn-end-tip", "Megjegyzés a kapcsolat végén"), + ("input note here", "Megjegyzés beírása"), + ("note-at-conn-end-tip", "Kérjen megjegyzést a kapcsolat végén"), ("Show terminal extra keys", "További terminálgombok megjelenítése"), - ("Relative mouse mode", ""), - ("rel-mouse-not-supported-peer-tip", ""), - ("rel-mouse-not-ready-tip", ""), - ("rel-mouse-lock-failed-tip", ""), - ("rel-mouse-exit-{}-tip", ""), - ("rel-mouse-permission-lost-tip", ""), - ("Changelog", ""), + ("Relative mouse mode", "Relatív egér mód"), + ("rel-mouse-not-supported-peer-tip", "A célkészülék nem támogatja a relatív egér módot."), + ("rel-mouse-not-ready-tip", "A relatív egér mód még nem áll készen. Kérjük, próbálkozzon később újra!"), + ("rel-mouse-lock-failed-tip", "Az egér nem zárolható, a relatív egér mód le van tiltva."), + ("rel-mouse-exit-{}-tip", "A kilépéshez nyomja meg a {} gombot."), + ("rel-mouse-permission-lost-tip", "A billentyűzet engedélyei visszavonásra kerültek. A relatív egér mód letiltásra került."), + ("Changelog", "Változásnapló"), ].iter().cloned().collect(); } From 9808d585cf248ab82c1186b6b18cdaa28f0a94cd Mon Sep 17 00:00:00 2001 From: minh <88567043+MinhAnime@users.noreply.github.com> Date: Tue, 13 Jan 2026 10:00:16 +0700 Subject: [PATCH 081/277] Update vi.rs file (#14027) --- src/lang/vi.rs | 924 ++++++++++++++++++++++++------------------------- 1 file changed, 462 insertions(+), 462 deletions(-) diff --git a/src/lang/vi.rs b/src/lang/vi.rs index dbfa11da6..3d03966da 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -3,10 +3,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Trạng thái hiện tại"), ("Your Desktop", "Desktop của bạn"), - ("desk_tip", "Desktop của bạn có thể đuợc truy cập bằng ID và mật khẩu này."), + ("desk_tip", "Desktop của bạn có thể được truy cập bằng ID và mật khẩu này."), ("Password", "Mật khẩu"), ("Ready", "Sẵn sàng"), - ("Established", "Đã đuợc thiết lập"), + ("Established", "Đã được thiết lập"), ("connecting_status", "Đang kết nối đến mạng lưới RustDesk..."), ("Enable service", "Bật dịch vụ"), ("Start service", "Bắt đầu dịch vụ"), @@ -16,19 +16,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Control Remote Desktop", "Điều khiển Desktop Từ Xa"), ("Transfer file", "Truyền Tệp Tin"), ("Connect", "Kết nối"), - ("Recent sessions", "Các session gần đây"), - ("Address book", "Quyển địa chỉ"), + ("Recent sessions", "Các phiên gần đây"), + ("Address book", "Sổ địa chỉ"), ("Confirmation", "Xác nhận"), ("TCP tunneling", "TCP tunneling"), ("Remove", "Loại bỏ"), ("Refresh random password", "Làm mới mật khẩu ngẫu nhiên"), ("Set your own password", "Đặt mật khẩu riêng"), ("Enable keyboard/mouse", "Cho phép sử dụng bàn phím/chuột"), - ("Enable clipboard", "Cho phép sử dụng clipboard"), + ("Enable clipboard", "Cho phép sử dụng Clipboard"), ("Enable file transfer", "Cho phép truyền tệp tin"), ("Enable TCP tunneling", "Cho phép TCP tunneling"), - ("IP Whitelisting", "Cho phép IP"), - ("ID/Relay Server", "Máy chủ ID/chuyển tiếp"), + ("IP Whitelisting", "Danh sách trắng IP"), + ("ID/Relay Server", "Máy chủ ID/Chuyển tiếp"), ("Import server config", "Nhập cấu hình máy chủ"), ("Export Server Config", "Xuất cấu hình máy chủ"), ("Import server configuration successfully", "Nhập cấu hình máy chủ thành công"), @@ -38,20 +38,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop service", "Dừng dịch vụ"), ("Change ID", "Thay đổi ID"), ("Your new ID", "ID mới của bạn"), - ("length %min% to %max%", "độ dài %min% đến %max%"), - ("starts with a letter", "bắt đầu bằng một chữ"), - ("allowed characters", "các ký tự cho phép"), - ("id_change_tip", "Các kí tự đuợc phép là: từ a-z, A-Z, 0-9, - (dash) và _ (dấu gạch dưới). Kí tự đầu tiên phải bắt đầu từ a-z, A-Z. Độ dài kí tự từ 6 đến 16"), + ("length %min% to %max%", "độ dài từ %min% đến %max%"), + ("starts with a letter", "bắt đầu bằng một chữ cái"), + ("allowed characters", "các ký tự được phép"), + ("id_change_tip", "Các ký tự được phép: a-z, A-Z, 0-9, - (gạch ngang) và _ (gạch dưới). Ký tự đầu tiên phải là chữ cái. Độ dài từ 6 đến 16."), ("Website", "Trang web"), ("About", "Giới thiệu"), - ("Slogan_tip", ""), - ("Privacy Statement", "Bảo Mật Thông tin"), + ("Slogan_tip", "Được tạo ra với sự tận tâm trong thế giới đầy hỗn loạn này!"), + ("Privacy Statement", "Chính sách bảo mật"), ("Mute", "Tắt tiếng"), - ("Build Date", "Ngày xuất bản"), + ("Build Date", "Ngày đóng gói"), ("Version", "Phiên bản"), ("Home", "Trang chủ"), ("Audio Input", "Đầu vào âm thanh"), - ("Enhancements", "Các tiện ích"), + ("Enhancements", "Tiện ích mở rộng"), ("Hardware Codec", "Codec phần cứng"), ("Adaptive bitrate", "Bitrate thích ứng"), ("ID Server", "Máy chủ ID"), @@ -59,37 +59,37 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Máy chủ API"), ("invalid_http", "phải bắt đầu bằng http:// hoặc https://"), ("Invalid IP", "IP không hợp lệ"), - ("Invalid format", "Định dạng không hợp lệnh"), - ("server_not_support", "Chưa đuợc hỗ trợ bởi máy chủ"), - ("Not available", "Chưa có mặt"), - ("Too frequent", "Quá thường xuyên"), + ("Invalid format", "Định dạng không hợp lệ"), + ("server_not_support", "Máy chủ chưa hỗ trợ"), + ("Not available", "Không khả dụng"), + ("Too frequent", "Thao tác quá thường xuyên"), ("Cancel", "Hủy"), ("Skip", "Bỏ qua"), ("Close", "Đóng"), ("Retry", "Thử lại"), ("OK", "OK"), ("Password Required", "Yêu cầu mật khẩu"), - ("Please enter your password", "Mời nhập mật khẩu"), + ("Please enter your password", "Vui lòng nhập mật khẩu"), ("Remember password", "Nhớ mật khẩu"), ("Wrong Password", "Sai mật khẩu"), ("Do you want to enter again?", "Bạn có muốn nhập lại không?"), - ("Connection Error", "Kết nối bị lỗi"), + ("Connection Error", "Lỗi kết nối"), ("Error", "Lỗi"), - ("Reset by the peer", "Đựoc cài đặt lại bởi người dùng từ xa"), + ("Reset by the peer", "Phía đối tác đã đặt lại kết nối"), ("Connecting...", "Đang kết nối..."), - ("Connection in progress. Please wait.", "Đang kết nối. Vui lòng chờ."), - ("Please try 1 minute later", "Hãy thử lại sau 1 phút"), - ("Login Error", "Đăng nhập bị lỗi"), + ("Connection in progress. Please wait.", "Đang thiết lập kết nối. Vui lòng chờ."), + ("Please try 1 minute later", "Vui lòng thử lại sau 1 phút"), + ("Login Error", "Lỗi đăng nhập"), ("Successful", "Thành công"), ("Connected, waiting for image...", "Đã kết nối, đang đợi hình ảnh..."), ("Name", "Tên"), ("Type", "Loại"), - ("Modified", "Chỉnh sửa"), + ("Modified", "Ngày chỉnh sửa"), ("Size", "Kích cỡ"), - ("Show Hidden Files", "Hiển thị tệp tin bị ẩn"), + ("Show Hidden Files", "Hiện tệp ẩn"), ("Receive", "Nhận"), ("Send", "Gửi"), - ("Refresh File", "Làm mới tệp tin"), + ("Refresh File", "Làm mới tệp"), ("Local", "Cục bộ"), ("Remote", "Từ xa"), ("Remote Computer", "Máy tính từ xa"), @@ -100,22 +100,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Multi Select", "Chọn nhiều"), ("Select All", "Chọn tất cả"), ("Unselect All", "Bỏ chọn tất cả"), - ("Empty Directory", "Thư mục rỗng"), - ("Not an empty directory", "Không phải thư mục rỗng"), - ("Are you sure you want to delete this file?", "Bạn chắc bạn có muốn xóa tệp tin này không?"), - ("Are you sure you want to delete this empty directory?", "Bạn chắc bạn có muốn xóa thư mục rỗng này không?"), - ("Are you sure you want to delete the file of this directory?", "Bạn chắc bạn có muốn xóa những tệp tin trong thư mục này không?"), - ("Do this for all conflicts", "Xác nhận đối với tất cả các trùng lặp"), - ("This is irreversible!", "Không thể hoàn tác!"), + ("Empty Directory", "Thư mục trống"), + ("Not an empty directory", "Thư mục không trống"), + ("Are you sure you want to delete this file?", "Bạn có chắc chắn muốn xóa tệp này không?"), + ("Are you sure you want to delete this empty directory?", "Bạn có chắc chắn muốn xóa thư mục trống này không?"), + ("Are you sure you want to delete the file of this directory?", "Bạn có chắc chắn muốn xóa các tệp trong thư mục này không?"), + ("Do this for all conflicts", "Áp dụng cho mọi xung đột"), + ("This is irreversible!", "Hành động này không thể hoàn tác!"), ("Deleting", "Đang xóa"), - ("files", "các tệp tin"), + ("files", "tệp"), ("Waiting", "Đang chờ"), ("Finished", "Hoàn thành"), ("Speed", "Tốc độ"), - ("Custom Image Quality", "Chất lượng hình ảnh"), + ("Custom Image Quality", "Tùy chỉnh chất lượng hình ảnh"), ("Privacy mode", "Chế độ riêng tư"), - ("Block user input", "Chặn các tương tác từ người dùng"), - ("Unblock user input", "Hủy chặn các tương tác từ người dùng"), + ("Block user input", "Chặn tương tác người dùng"), + ("Unblock user input", "Hủy chặn tương tác người dùng"), ("Adjust Window", "Điều chỉnh cửa sổ"), ("Original", "Gốc"), ("Shrink", "Thu nhỏ"), @@ -124,216 +124,216 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ScrollAuto", "Tự động cuộn"), ("Good image quality", "Chất lượng hình ảnh tốt"), ("Balanced", "Cân bằng"), - ("Optimize reaction time", "Tối ưu thời gian phản ứng"), + ("Optimize reaction time", "Tối ưu thời gian phản hồi"), ("Custom", "Tùy chỉnh"), - ("Show remote cursor", "Hiển thị con trỏ từ máy từ xa"), - ("Show quality monitor", "Hiện thị chất lượng của màn hình"), - ("Disable clipboard", "Tắt clipboard"), - ("Lock after session end", "Khóa sau khi kết thúc phiên kết nối"), - ("Insert Ctrl + Alt + Del", "Cài Ctrl + Alt + Del"), - ("Insert Lock", "Cài khóa"), + ("Show remote cursor", "Hiện con trỏ từ xa"), + ("Show quality monitor", "Hiện thông tin chất lượng"), + ("Disable clipboard", "Tắt Clipboard"), + ("Lock after session end", "Khóa máy sau khi kết thúc"), + ("Insert Ctrl + Alt + Del", "Gửi Ctrl + Alt + Del"), + ("Insert Lock", "Khóa máy"), ("Refresh", "Làm mới"), ("ID does not exist", "ID không tồn tại"), - ("Failed to connect to rendezvous server", "Không thể kết nối đến máy chủ rendezvous"), - ("Please try later", "Thử lại sau"), - ("Remote desktop is offline", "Máy tính từ xa hiện đang ngoại tuyến"), - ("Key mismatch", "Chìa không khớp"), + ("Failed to connect to rendezvous server", "Không thể kết nối đến máy chủ Rendezvous"), + ("Please try later", "Vui lòng thử lại sau"), + ("Remote desktop is offline", "Máy tính từ xa đang ngoại tuyến"), + ("Key mismatch", "Khóa không khớp"), ("Timeout", "Quá thời gian"), - ("Failed to connect to relay server", "Không thể kết nối tới máy chủ chuyển tiếp"), - ("Failed to connect via rendezvous server", "Không thể kết nối qua máy chủ rendezvous"), - ("Failed to connect via relay server", "Không thể kết nối qua máy chủ chuyển tiếp"), - ("Failed to make direct connection to remote desktop", "Không thể kết nối thẳng tới máy tính từ xa"), - ("Set Password", "Cài đặt mật khẩu"), + ("Failed to connect to relay server", "Không thể kết nối tới máy chủ Chuyển tiếp"), + ("Failed to connect via rendezvous server", "Không thể kết nối qua máy chủ Rendezvous"), + ("Failed to connect via relay server", "Không thể kết nối qua máy chủ Chuyển tiếp"), + ("Failed to make direct connection to remote desktop", "Không thể kết nối trực tiếp"), + ("Set Password", "Đặt mật khẩu"), ("OS Password", "Mật khẩu hệ điều hành"), - ("install_tip", "Do UAC, RustDesk sẽ không thể hoạt động đúng cách là bên từ xa trong vài trường hợp. Để tránh UAC, hãy nhấn cái nút dưới đây để cài RustDesk vào hệ thống."), + ("install_tip", "Do cơ chế UAC, RustDesk có thể không hoạt động ổn định ở phía người dùng từ xa trong một số trường hợp. Để tránh vấn đề này, vui lòng nhấn nút bên dưới để cài đặt RustDesk vào hệ thống."), ("Click to upgrade", "Nhấn để nâng cấp"), ("Configure", "Cấu hình"), - ("config_acc", "Để có thể điều khiển máy tính từ xa, bạn cần phải cung cấp quyền \"Trợ năng\" cho RustDesk"), - ("config_screen", "Để có thể truy cập máy tính từ xa, bạn cần phải cung cấp quyền \"Ghi Màn Hình\" cho RustDesk."), - ("Installing ...", "Đang cài đặt ..."), - ("Install", "Cài"), - ("Installation", "Cài"), + ("config_acc", "Để điều khiển từ xa, bạn cần cấp quyền \"Trợ năng\" cho RustDesk."), + ("config_screen", "Để truy cập từ xa, bạn cần cấp quyền \"Ghi màn hình\" cho RustDesk."), + ("Installing ...", "Đang cài đặt..."), + ("Install", "Cài đặt"), + ("Installation", "Cài đặt"), ("Installation Path", "Đường dẫn cài đặt"), - ("Create start menu shortcuts", "Tạo shortcut tại start menu"), - ("Create desktop icon", "Tạo biểu tượng trên màn hình chính"), - ("agreement_tip", "Bằng cách bắt đầu cài đặt, bạn chấp nhận thỏa thuận cấp phép."), - ("Accept and Install", "Chấp nhận và Cài đặtđặt"), - ("End-user license agreement", "Thỏa thuận cấp phép dành cho người dùng"), - ("Generating ...", "Đang tạo ..."), - ("Your installation is lower version.", "Phiên bản của bạn là phiên bản cũ"), - ("not_close_tcp_tip", "Đừng đóng cửa sổ này khi bạn đang sử dụng tunnel"), - ("Listening ...", "Đang nghe ..."), - ("Remote Host", "Máy từ xa"), + ("Create start menu shortcuts", "Tạo shortcut ở Start Menu"), + ("Create desktop icon", "Tạo biểu tượng ngoài màn hình"), + ("agreement_tip", "Bằng việc bắt đầu cài đặt, bạn đồng ý với các điều khoản cấp phép."), + ("Accept and Install", "Chấp nhận và Cài đặt"), + ("End-user license agreement", "Thỏa thuận người dùng cuối"), + ("Generating ...", "Đang khởi tạo..."), + ("Your installation is lower version.", "Phiên bản cài đặt của bạn cũ hơn."), + ("not_close_tcp_tip", "Đừng đóng cửa sổ này khi đang sử dụng Tunnel"), + ("Listening ...", "Đang lắng nghe..."), + ("Remote Host", "Máy chủ từ xa"), ("Remote Port", "Cổng từ xa"), ("Action", "Hành động"), ("Add", "Thêm"), ("Local Port", "Cổng nội bộ"), ("Local Address", "Địa chỉ nội bộ"), - ("Change Local Port", "Thay đổi cổng nội bộ"), - ("setup_server_tip", "Để kết nối nhanh hơn, hãy tự tạo máy chủ riêng"), - ("Too short, at least 6 characters.", "Quá ngắn, độ dài phải ít nhất là 6."), - ("The confirmation is not identical.", "Xác minh không khớp"), + ("Change Local Port", "Đổi cổng nội bộ"), + ("setup_server_tip", "Để kết nối nhanh hơn, hãy tự thiết lập máy chủ riêng"), + ("Too short, at least 6 characters.", "Quá ngắn, cần ít nhất 6 ký tự."), + ("The confirmation is not identical.", "Mật khẩu xác nhận không khớp"), ("Permissions", "Quyền"), ("Accept", "Chấp nhận"), ("Dismiss", "Bỏ qua"), ("Disconnect", "Ngắt kết nối"), - ("Enable file copy and paste", "Cho phép sao chép và dán tệp tin"), + ("Enable file copy and paste", "Cho phép sao chép và dán tệp"), ("Connected", "Đã kết nối"), - ("Direct and encrypted connection", "Kết nối trực tiếp và đuợc mã hóa"), + ("Direct and encrypted connection", "Kết nối trực tiếp và mã hóa"), ("Relayed and encrypted connection", "Kết nối chuyển tiếp và mã hóa"), - ("Direct and unencrypted connection", "Kết nối trực tiếp và không đuợc mã hóa"), - ("Relayed and unencrypted connection", "Kết nối chuyển tiếp và không đuợc mã hóa"), + ("Direct and unencrypted connection", "Kết nối trực tiếp và không mã hóa"), + ("Relayed and unencrypted connection", "Kết nối chuyển tiếp và không mã hóa"), ("Enter Remote ID", "Nhập ID từ xa"), - ("Enter your password", "Nhập mật khẩu"), - ("Logging in...", "Đang đăng nhập"), - ("Enable RDP session sharing", "Cho phép chia sẻ phiên kết nối RDP"), + ("Enter your password", "Nhập mật khẩu của bạn"), + ("Logging in...", "Đang đăng nhập..."), + ("Enable RDP session sharing", "Cho phép chia sẻ phiên RDP"), ("Auto Login", "Tự động đăng nhập"), - ("Enable direct IP access", "Cho phép truy cập trực tiếp qua IP"), + ("Enable direct IP access", "Cho phép truy cập IP trực tiếp"), ("Rename", "Đổi tên"), - ("Space", "Dấu cách"), - ("Create desktop shortcut", "Tạo shortcut trên desktop"), - ("Change Path", "Đổi địa điểm"), + ("Space", "Khoảng cách"), + ("Create desktop shortcut", "Tạo shortcut màn hình"), + ("Change Path", "Đổi đường dẫn"), ("Create Folder", "Tạo thư mục"), - ("Please enter the folder name", "Hãy nhập tên thư mục"), - ("Fix it", "Sửa nó"), + ("Please enter the folder name", "Vui lòng nhập tên thư mục"), + ("Fix it", "Sửa lỗi"), ("Warning", "Cảnh báo"), - ("Login screen using Wayland is not supported", "Màn hình đăng nhập sử dụng Wayland không được hỗ trợ"), + ("Login screen using Wayland is not supported", "Màn hình đăng nhập Wayland không được hỗ trợ"), ("Reboot required", "Yêu cầu khởi động lại"), - ("Unsupported display server", "Máy chủ hiển thị không đuợc hỗ trọ"), - ("x11 expected", "Cần x11"), + ("Unsupported display server", "Máy chủ hiển thị không được hỗ trợ"), + ("x11 expected", "Yêu cầu X11"), ("Port", "Cổng"), ("Settings", "Cài đặt"), ("Username", "Tên người dùng"), ("Invalid port", "Cổng không hợp lệ"), - ("Closed manually by the peer", "Đã đóng thủ công bởi người dùng từ xa"), - ("Enable remote configuration modification", "Cho phép thay đổi cấu hình bên từ xa"), - ("Run without install", "Chạy mà không cần cài đặt"), - ("Connect via relay", "Kết nối qua máy chủ chuyển tiếp"), - ("Always connect via relay", "Luôn kết nối qua máy chủ chuyển tiếp"), - ("whitelist_tip", "Chỉ có những IP đựoc cho phép mới có thể truy cập"), + ("Closed manually by the peer", "Bị đóng thủ công bởi đối tác"), + ("Enable remote configuration modification", "Cho phép sửa cấu hình từ xa"), + ("Run without install", "Chạy không cần cài đặt"), + ("Connect via relay", "Kết nối qua chuyển tiếp"), + ("Always connect via relay", "Luôn kết nối qua chuyển tiếp"), + ("whitelist_tip", "Chỉ IP trong danh sách trắng mới có thể truy cập"), ("Login", "Đăng nhập"), ("Verify", "Xác thực"), - ("Remember me", "Nhớ tài khoản"), - ("Trust this device", "Tin thiết bị này"), + ("Remember me", "Ghi nhớ"), + ("Trust this device", "Tin tưởng thiết bị này"), ("Verification code", "Mã xác thực"), - ("verification_tip", "Bạn đang đăng nhập trên một thiết bị mới, một mã xác thực đã được gửi tới email đăng ký của bạn, hãy nhập mã xác thực để tiếp tục đăng nhập."), + ("verification_tip", "Bạn đang đăng nhập trên thiết bị mới. Một mã xác thực đã được gửi đến email của bạn, vui lòng nhập mã để tiếp tục."), ("Logout", "Đăng xuất"), ("Tags", "Thẻ"), ("Search ID", "Tìm ID"), - ("whitelist_sep", "Đuợc cách nhau bởi dấu phẩy, dấu chấm phẩy, dấu cách hay dòng mới"), + ("whitelist_sep", "Phân cách bởi dấu phẩy, dấu chấm phẩy, khoảng trắng hoặc dòng mới"), ("Add ID", "Thêm ID"), ("Add Tag", "Thêm thẻ"), - ("Unselect all tags", "Hủy chọn tất cả các thẻ"), + ("Unselect all tags", "Bỏ chọn tất cả thẻ"), ("Network error", "Lỗi mạng"), - ("Username missed", "Mất tên người dùng"), - ("Password missed", "Mất mật khẩu"), - ("Wrong credentials", "Chứng danh bị sai"), + ("Username missed", "Thiếu tên người dùng"), + ("Password missed", "Thiếu mật khẩu"), + ("Wrong credentials", "Thông tin đăng nhập sai"), ("The verification code is incorrect or has expired", "Mã xác thực không đúng hoặc đã hết hạn"), - ("Edit Tag", "Chỉnh sửa thẻthẻ"), + ("Edit Tag", "Sửa thẻ"), ("Forget Password", "Quên mật khẩu"), - ("Favorites", "Ưa thích"), - ("Add to Favorites", "Thêm vào mục Ưa thích"), - ("Remove from Favorites", "Xóa khỏi mục Ưa thích"), + ("Favorites", "Yêu thích"), + ("Add to Favorites", "Thêm vào yêu thích"), + ("Remove from Favorites", "Xóa khỏi yêu thích"), ("Empty", "Trống"), ("Invalid folder name", "Tên thư mục không hợp lệ"), - ("Socks5 Proxy", "Proxy Socks5"), - ("Socks5/Http(s) Proxy", "Proxy Socks5/Http(s)"), - ("Discovered", "Đuợc phát hiện"), - ("install_daemon_tip", "Để chạy lúc khởi động máy, bạn cần phải cài dịch vụ hệ thống."), + ("Socks5 Proxy", "Socks5 Proxy"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"), + ("Discovered", "Đã phát hiện"), + ("install_daemon_tip", "Để khởi động cùng hệ thống, bạn cần cài đặt dịch vụ daemon."), ("Remote ID", "ID từ xa"), ("Paste", "Dán"), - ("Paste here?", "Dán ở đây?"), - ("Are you sure to close the connection?", "Bạn có chắc muốn đóng kết nối không"), - ("Download new version", "Tải về phiên bản mới"), + ("Paste here?", "Dán vào đây?"), + ("Are you sure to close the connection?", "Bạn có chắc chắn muốn đóng kết nối?"), + ("Download new version", "Tải phiên bản mới"), ("Touch mode", "Chế độ chạm"), - ("Mouse mode", "Chế độ dùng chuột"), - ("One-Finger Tap", "Chạm bằng một ngón tay"), + ("Mouse mode", "Chế độ chuột"), + ("One-Finger Tap", "Chạm một ngón"), ("Left Mouse", "Chuột trái"), - ("One-Long Tap", "Chạm lâu bằng một ngón tay"), - ("Two-Finger Tap", "Chạm bằng hai ngón tay"), + ("One-Long Tap", "Chạm giữ một ngón"), + ("Two-Finger Tap", "Chạm hai ngón"), ("Right Mouse", "Chuột phải"), - ("One-Finger Move", "Di chuyển bằng một ngón tay"), - ("Double Tap & Move", "Chạm hai lần và di chuyển"), - ("Mouse Drag", "Di chuyển bằng chuột"), - ("Three-Finger vertically", "Ba ngón tay theo chiều dọc"), - ("Mouse Wheel", "Bánh xe lăn trê con chuột"), - ("Two-Finger Move", "Di chuyển bằng hai ngón tay"), - ("Canvas Move", "Di chuyển canvas"), - ("Pinch to Zoom", "Véo để phóng to/nhỏ"), - ("Canvas Zoom", "Phóng to/nhỏ canvas"), - ("Reset canvas", "Cài đặt lại canvas"), - ("No permission of file transfer", "Không có quyền truyền tệp tin"), - ("Note", "Ghi nhớ"), + ("One-Finger Move", "Di chuyển một ngón"), + ("Double Tap & Move", "Chạm đúp và di chuyển"), + ("Mouse Drag", "Kéo chuột"), + ("Three-Finger vertically", "Ba ngón theo chiều dọc"), + ("Mouse Wheel", "Con lăn chuột"), + ("Two-Finger Move", "Di chuyển hai ngón"), + ("Canvas Move", "Di chuyển khung hình"), + ("Pinch to Zoom", "Véo để thu phóng"), + ("Canvas Zoom", "Thu phóng khung hình"), + ("Reset canvas", "Đặt lại khung hình"), + ("No permission of file transfer", "Không có quyền truyền tệp"), + ("Note", "Ghi chú"), ("Connection", "Kết nối"), ("Share screen", "Chia sẻ màn hình"), - ("Chat", "Chat"), - ("Total", "Tổng"), - ("items", "items"), - ("Selected", "Đã đuợc chọn"), - ("Screen Capture", "Ghi màn hình"), - ("Input Control", "Điều khiển đầu vào"), + ("Chat", "Trò chuyện"), + ("Total", "Tổng cộng"), + ("items", "mục"), + ("Selected", "Đã chọn"), + ("Screen Capture", "Chụp màn hình"), + ("Input Control", "Kiểm soát đầu vào"), ("Audio Capture", "Ghi âm thanh"), - ("Do you accept?", "Bạn có chấp nhận không?"), + ("Do you accept?", "Bạn có đồng ý không?"), ("Open System Setting", "Mở cài đặt hệ thống"), - ("How to get Android input permission?", "Cách để có quyền nhập trên Android?"), - ("android_input_permission_tip1", "Để thiết bị từ xa điều khiển thiết bị Android của bạn bằng chuột hoặc chạm, bạn cần cho phép RustDesk sử dụng dịch vụ \"Trợ năng\"."), - ("android_input_permission_tip2", "Vui lòng chuyển đến trang cài đặt hệ thống tiếp theo, tìm và nhập [Dịch vụ đã cài đặt], bật dịch vụ [RustDesk Input]."), - ("android_new_connection_tip", "Yêu cầu kiểm soát mới đã được nhận, yêu cầu này muốn kiểm soát thiết bị hiện tại của bạn."), - ("android_service_will_start_tip", "Bật \"Ghi màn hình\" sẽ tự động khởi động dịch vụ, cho phép các thiết bị khác yêu cầu kết nối với thiết bị của bạn."), - ("android_stop_service_tip", "Đóng dịch vụ sẽ tự động đóng tất cả các kết nối đã thiết lập."), - ("android_version_audio_tip", "Phiên bản Android hiện tại không hỗ trợ ghi âm, vui lòng nâng cấp lên Android 10 trở lên."), - ("android_start_service_tip", "Nhấn [Bắt đầu dịch vụ] hoặc bật quyền [Ghi màn hình] để bắt đầu dịch vụ chia sẻ màn hình"), - ("android_permission_may_not_change_tip", "Quyền cho các kết nối đã được thiếp lập có thể không được thay đổi ngay cho tới khi kết nối lại"), + ("How to get Android input permission?", "Làm sao để lấy quyền nhập liệu trên Android?"), + ("android_input_permission_tip1", "Để điều khiển Android bằng chuột hoặc chạm, bạn cần cấp quyền [Trợ năng]."), + ("android_input_permission_tip2", "Vui lòng tìm [Dịch vụ đã cài đặt] trong cài đặt và bật [RustDesk Input]."), + ("android_new_connection_tip", "Yêu cầu điều khiển mới đã được nhận."), + ("android_service_will_start_tip", "Bật [Ghi màn hình] sẽ tự động khởi động dịch vụ."), + ("android_stop_service_tip", "Dừng dịch vụ sẽ đóng tất cả các kết nối."), + ("android_version_audio_tip", "Phiên bản Android này không hỗ trợ ghi âm, vui lòng nâng cấp lên Android 10+."), + ("android_start_service_tip", "Nhấn [Bắt đầu dịch vụ] để chia sẻ màn hình."), + ("android_permission_may_not_change_tip", "Quyền có thể không thay đổi ngay lập tức cho đến khi kết nối lại."), ("Account", "Tài khoản"), ("Overwrite", "Ghi đè"), - ("This file exists, skip or overwrite this file?", "Tệp tin này đã tồn tại, bạn có muốn bỏ qua hay ghi đè lên tệp tin này?"), + ("This file exists, skip or overwrite this file?", "Tệp đã tồn tại, bỏ qua hay ghi đè?"), ("Quit", "Thoát"), ("Help", "Trợ giúp"), ("Failed", "Thất bại"), ("Succeeded", "Thành công"), - ("Someone turns on privacy mode, exit", "Ai đó đã bật chế độ riêng tư, thoát"), + ("Someone turns on privacy mode, exit", "Chế độ riêng tư đã được bật, thoát"), ("Unsupported", "Không hỗ trợ"), - ("Peer denied", "Người dùng từ xa đã từ chối"), - ("Please install plugins", "Hãy cài plugins"), - ("Peer exit", "Người dùng từ xa đã thoát"), + ("Peer denied", "Đối tác từ chối"), + ("Please install plugins", "Vui lòng cài đặt plugin"), + ("Peer exit", "Đối tác đã thoát"), ("Failed to turn off", "Không thể tắt"), ("Turned off", "Đã tắt"), ("Language", "Ngôn ngữ"), - ("Keep RustDesk background service", "Giữ dịch vụ nền RustDesk"), - ("Ignore Battery Optimizations", "Bỏ qua các tối ưu pin"), - ("android_open_battery_optimizations_tip", "Nếu bạn muốn tắt tính năng này, vui lòng chuyển đến trang cài đặt ứng dụng RustDesk tiếp theo, tìm và nhập [Pin], Bỏ chọn [Không hạn chế]"), - ("Start on boot", "Chạy khi khởi động"), - ("Start the screen sharing service on boot, requires special permissions", "Chạy dịch vụ chia sẻ màn hình khi khởi động, yêu cầu quyền đặc biệt"), - ("Connection not allowed", "Kết nối không đuợc phép"), + ("Keep RustDesk background service", "Giữ dịch vụ RustDesk chạy nền"), + ("Ignore Battery Optimizations", "Bỏ qua tối ưu hóa pin"), + ("android_open_battery_optimizations_tip", "Vui lòng chọn [Không hạn chế] trong cài đặt Pin."), + ("Start on boot", "Khởi động cùng hệ thống"), + ("Start the screen sharing service on boot, requires special permissions", "Khởi động dịch vụ chia sẻ màn hình khi bật máy (cần quyền đặc biệt)"), + ("Connection not allowed", "Kết nối không được phép"), ("Legacy mode", "Chế độ cũ"), - ("Map mode", "Chế độ map"), - ("Translate mode", "Chế độ phiên dịch"), - ("Use permanent password", "Sử dụng mật khẩu vĩnh viễn"), - ("Use both passwords", "Sử dụng cả hai mật khẩu"), + ("Map mode", "Chế độ bản đồ"), + ("Translate mode", "Chế độ dịch"), + ("Use permanent password", "Dùng mật khẩu vĩnh viễn"), + ("Use both passwords", "Dùng cả hai mật khẩu"), ("Set permanent password", "Đặt mật khẩu vĩnh viễn"), - ("Enable remote restart", "Bật khởi động lại từ xa"), - ("Restart remote device", "Khởi động lại thiết bị từ xa"), - ("Are you sure you want to restart", "Bạn có chắc bạn muốn khởi động lại không"), - ("Restarting remote device", "Đang khởi động lại thiết bị từ xa"), - ("remote_restarting_tip", "Thiết bị từ xa đang khởi động lại, hãy đóng cửa sổ tin nhắn này và kết nối lại với mật khẩu vĩnh viễn sau một khoảng thời gian"), + ("Enable remote restart", "Cho phép khởi động lại từ xa"), + ("Restart remote device", "Khởi động lại máy từ xa"), + ("Are you sure you want to restart", "Bạn có chắc chắn muốn khởi động lại?"), + ("Restarting remote device", "Đang khởi động lại máy từ xa..."), + ("remote_restarting_tip", "Máy từ xa đang khởi động lại, vui lòng kết nối lại sau ít phút."), ("Copied", "Đã sao chép"), ("Exit Fullscreen", "Thoát toàn màn hình"), ("Fullscreen", "Toàn màn hình"), - ("Mobile Actions", "Hành động trên thiết bị di động"), + ("Mobile Actions", "Thao tác di động"), ("Select Monitor", "Chọn màn hình"), - ("Control Actions", "Kiểm soát hành động"), - ("Display Settings", "Thiết lập hiển thị"), - ("Ratio", "Tỉ lệ"), + ("Control Actions", "Thao tác điều khiển"), + ("Display Settings", "Cài đặt hiển thị"), + ("Ratio", "Tỷ lệ"), ("Image Quality", "Chất lượng hình ảnh"), ("Scroll Style", "Kiểu cuộn"), ("Show Toolbar", "Hiện thanh công cụ"), ("Hide Toolbar", "Ẩn thanh công cụ"), ("Direct Connection", "Kết nối trực tiếp"), ("Relay Connection", "Kết nối chuyển tiếp"), - ("Secure Connection", "Kết nối an toàn"), - ("Insecure Connection", "Kết nối không an toàn"), - ("Scale original", "Quy mô gốc"), - ("Scale adaptive", "Quy mô thích ứng"), + ("Secure Connection", "Kết nối bảo mật"), + ("Insecure Connection", "Kết nối không bảo mật"), + ("Scale original", "Tỷ lệ gốc"), + ("Scale adaptive", "Tỷ lệ thích ứng"), ("General", "Chung"), ("Security", "Bảo mật"), ("Theme", "Chủ đề"), @@ -342,112 +342,112 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Dark", "Tối"), ("Light", "Sáng"), ("Follow System", "Theo hệ thống"), - ("Enable hardware codec", "Bật codec phần cứng"), + ("Enable hardware codec", "Bật Codec phần cứng"), ("Unlock Security Settings", "Mở khóa cài đặt bảo mật"), ("Enable audio", "Bật âm thanh"), ("Unlock Network Settings", "Mở khóa cài đặt mạng"), ("Server", "Máy chủ"), - ("Direct IP Access", "Truy cập trực tiếp qua IP"), - ("Proxy", ""), + ("Direct IP Access", "Truy cập IP trực tiếp"), + ("Proxy", "Proxy"), ("Apply", "Áp dụng"), - ("Disconnect all devices?", "Ngắt kết nối tất cả thiết bị"), - ("Clear", "Làm trống"), - ("Audio Input Device", "Thiết bị âm thanh đầu vào"), - ("Use IP Whitelisting", "Dùng danh sách các IP cho phép"), + ("Disconnect all devices?", "Ngắt tất cả thiết bị?"), + ("Clear", "Xóa sạch"), + ("Audio Input Device", "Thiết bị đầu vào âm thanh"), + ("Use IP Whitelisting", "Sử dụng danh sách trắng IP"), ("Network", "Mạng"), ("Pin Toolbar", "Ghim thanh công cụ"), ("Unpin Toolbar", "Bỏ ghim thanh công cụ"), ("Recording", "Đang ghi hình"), ("Directory", "Thư mục"), - ("Automatically record incoming sessions", "Tự động ghi những phiên kết nối vào"), - ("Automatically record outgoing sessions", ""), + ("Automatically record incoming sessions", "Tự động ghi lại các kết nối đến"), + ("Automatically record outgoing sessions", "Tự động ghi lại các kết nối đi"), ("Change", "Thay đổi"), - ("Start session recording", "Bắt đầu ghi hình phiên kết nối"), - ("Stop session recording", "Dừng ghi hình phiên kết nối"), - ("Enable recording session", "Bật ghi hình phiên kết nối"), - ("Enable LAN discovery", "Bật phát hiện mạng nội bộ (LAN)"), - ("Deny LAN discovery", "Từ chối phát hiện mạng nội bộ (LAN)"), - ("Write a message", "Viết một tin nhắn"), - ("Prompt", ""), - ("Please wait for confirmation of UAC...", "Vui lòng chờ cho phép UAC"), - ("elevated_foreground_window_tip", "Cửa sổ hiện tại của máy tính từ xa yêu cầu quyền cao hơn để vận hành, nên bạn không thể sử dụng chuột và bàn phím tạm thời. Bạn có thể yêu cầu người dùng từ xa thu nhỏ cửa sổ hiện tại, hoặc nhấn vào nút Cấp Quyền trong cửa sổ quản lý kết nối. Để tránh tính trạng này, chúng tôi gợi ý nên cài đặt phần mềm ở phía thiết bị từ xa."), + ("Start session recording", "Bắt đầu ghi hình phiên"), + ("Stop session recording", "Dừng ghi hình phiên"), + ("Enable recording session", "Cho phép ghi hình phiên"), + ("Enable LAN discovery", "Bật phát hiện trong mạng LAN"), + ("Deny LAN discovery", "Từ chối phát hiện trong mạng LAN"), + ("Write a message", "Viết tin nhắn..."), + ("Prompt", "Gợi ý"), + ("Please wait for confirmation of UAC...", "Vui lòng chờ xác nhận UAC..."), + ("elevated_foreground_window_tip", "Cửa sổ phía trước yêu cầu quyền cao hơn, tạm thời không thể sử dụng chuột/phím. Yêu cầu phía đối tác thu nhỏ cửa sổ hoặc cấp quyền."), ("Disconnected", "Đã ngắt kết nối"), ("Other", "Khác"), - ("Confirm before closing multiple tabs", "Xác nhận trước khi đóng nhiều cửa sổ"), + ("Confirm before closing multiple tabs", "Xác nhận trước khi đóng nhiều tab"), ("Keyboard Settings", "Cài đặt bàn phím"), - ("Full Access", "Truy cập không giới hạng"), + ("Full Access", "Toàn quyền truy cập"), ("Screen Share", "Chia sẻ màn hình"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland yêu cầu phiên bản Ubuntu 21.04 trở lên."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland yêu cầu phiên bản distro linux cao hơn. Vui lòng thử máy tính để bàn X11 hoặc thay đổi hệ điều hành của bạn."), - ("JumpLink", "View"), - ("Please Select the screen to be shared(Operate on the peer side).", "Vui lòng Chọn màn hình để chia sẻ (Vận hành ở phía người dùng từ xa)."), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland yêu cầu Ubuntu 21.04 trở lên."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland yêu cầu phiên bản Linux mới hơn. Hãy thử X11 hoặc đổi hệ điều hành."), + ("JumpLink", "Xem"), + ("Please Select the screen to be shared(Operate on the peer side).", "Vui lòng chọn màn hình chia sẻ (Thao tác ở phía đối tác)."), ("Show RustDesk", "Hiện RustDesk"), - ("This PC", ""), + ("This PC", "Máy tính này"), ("or", "hoặc"), ("Continue with", "Tiếp tục với"), - ("Elevate", "Cấp Quyền"), - ("Zoom cursor", "Phóng to chuột"), - ("Accept sessions via password", "Chấp nhận phiên kết nối bằng mật khẩu"), - ("Accept sessions via click", "Chấp nhận phiên kết nối bằng chuột"), - ("Accept sessions via both", "Chấp nhận phiên kết nối bằng cả hai"), - ("Please wait for the remote side to accept your session request...", "Vui lòng chờ phía người dùng từ xa chấp nhận kết nối của bạn..."), - ("One-time Password", "Mật khẩu một lần"), - ("Use one-time password", "Dùng mật khẩu một lần"), + ("Elevate", "Nâng quyền"), + ("Zoom cursor", "Phóng to con trỏ"), + ("Accept sessions via password", "Chấp nhận phiên qua mật khẩu"), + ("Accept sessions via click", "Chấp nhận phiên qua xác nhận"), + ("Accept sessions via both", "Chấp nhận phiên qua cả hai"), + ("Please wait for the remote side to accept your session request...", "Vui lòng chờ phía đối tác chấp nhận yêu cầu kết nối..."), + ("One-time Password", "Mật khẩu dùng một lần"), + ("Use one-time password", "Sử dụng mật khẩu một lần"), ("One-time password length", "Độ dài mật khẩu một lần"), - ("Request access to your device", "Yêu cầu quyền truy cập vào thiết bị của bạn"), + ("Request access to your device", "Yêu cầu truy cập thiết bị của bạn"), ("Hide connection management window", "Ẩn cửa sổ quản lý kết nối"), - ("hide_cm_tip", "Cho phép ẩn chỉ khi chấp nhận phiên kết nối bằng mật khẩu vĩnh viễn"), - ("wayland_experiment_tip", "Hỗ trợ cho Wayland đang trong giai đoạn thử nghiệm, vui lòng dùng DX11 nếu bạn muốn sử dụng kết nối không giám sát."), - ("Right click to select tabs", "Chuột phải để chọn cửa sổ"), + ("hide_cm_tip", "Chỉ ẩn khi sử dụng mật khẩu vĩnh viễn"), + ("wayland_experiment_tip", "Wayland đang thử nghiệm, hãy dùng X11 nếu muốn ổn định."), + ("Right click to select tabs", "Chuột phải để chọn tab"), ("Skipped", "Đã bỏ qua"), - ("Add to address book", "Thêm vào Quyển địa chỉ"), + ("Add to address book", "Thêm vào sổ địa chỉ"), ("Group", "Nhóm"), - ("Search", "Tìm"), - ("Closed manually by web console", "Đã đóng thủ công bằng bảng điều khiển web"), + ("Search", "Tìm kiếm"), + ("Closed manually by web console", "Đã đóng bởi Web Console"), ("Local keyboard type", "Loại bàn phím cục bộ"), - ("Select local keyboard type", "Chọn kiểu bàn phím cục bộ"), - ("software_render_tip", "Nếu bạn đang dùng card đồ họa Nvidia trên Linux và cửa sổ từ xa bị tắt ngay lập tức sau khi kết nối, chuyển sang driver mã nguồn mở Nouveau và chọn sử dụng render bằng phần mềm có thể khắc phục được. Yêu cầu khởi động lại phần mềm."), - ("Always use software rendering", "Cho phép render bằng phần mềm"), - ("config_input", "Để điều khiển được máy tính từ xa với bàn phím, bạn cần cho phép RustDesk quyền \"Theo dõi đầu vào\" (Input Monitoring)"), - ("config_microphone", "Để nói chuyện từ xa, bạn phải cho phép RustDesk quyền \"Ghi âm thanh\" (Record Audio)"), - ("request_elevation_tip", "Bạn cũng có thể yêu cầu được cấp quyền nếu có người nào đó ở bên phía kết nối."), + ("Select local keyboard type", "Chọn loại bàn phím cục bộ"), + ("software_render_tip", "Nếu gặp lỗi hiển thị trên Linux với Nvidia, hãy thử phần mềm render."), + ("Always use software rendering", "Luôn sử dụng render bằng phần mềm"), + ("config_input", "Cấp quyền [Theo dõi đầu vào] để dùng bàn phím."), + ("config_microphone", "Cấp quyền [Ghi âm] để trò chuyện."), + ("request_elevation_tip", "Bạn cũng có thể yêu cầu nâng quyền từ người ở phía xa."), ("Wait", "Chờ"), - ("Elevation Error", "Cấp Quyền Lỗi"), + ("Elevation Error", "Lỗi nâng quyền"), ("Ask the remote user for authentication", "Yêu cầu người dùng từ xa xác thực"), - ("Choose this if the remote account is administrator", "Chọn cái này nếu tài khoản từ xa là quản trị viên"), - ("Transmit the username and password of administrator", "Truyền tên tài khoản và mật khẩu của quản trị viên"), - ("still_click_uac_tip", "Vẫn cần người dùng từ xa nhấn OK trên cửa sổ UAC của RustDesk đang chạy."), - ("Request Elevation", "Yêu cầu Cấp Quyền"), - ("wait_accept_uac_tip", "Vui lòng chờ cho người dùng từ xa chấp nhận cửa sổ UAC"), - ("Elevate successfully", "Cấp quyền thành công"), + ("Choose this if the remote account is administrator", "Chọn nếu tài khoản từ xa là Quản trị viên"), + ("Transmit the username and password of administrator", "Gửi tên đăng nhập và mật khẩu Quản trị viên"), + ("still_click_uac_tip", "Người dùng từ xa vẫn cần nhấn OK trên hộp thoại UAC."), + ("Request Elevation", "Yêu cầu nâng quyền"), + ("wait_accept_uac_tip", "Vui lòng chờ đối tác chấp nhận UAC."), + ("Elevate successfully", "Nâng quyền thành công"), ("uppercase", "chữ hoa"), ("lowercase", "chữ thường"), - ("digit", "chữ số"), + ("digit", "số"), ("special character", "ký tự đặc biệt"), - ("length>=8", "độ dài>=8"), + ("length>=8", "độ dài >= 8"), ("Weak", "Yếu"), ("Medium", "Trung bình"), - ("Strong", "Mạng"), + ("Strong", "Mạnh"), ("Switch Sides", "Đổi bên"), - ("Please confirm if you want to share your desktop?", "Vui lòng xác nhận nếu bạn muốn chia sẻ máy tính?"), + ("Please confirm if you want to share your desktop?", "Xác nhận chia sẻ màn hình?"), ("Display", "Hiển thị"), ("Default View Style", "Kiểu xem mặc định"), ("Default Scroll Style", "Kiểu cuộn mặc định"), ("Default Image Quality", "Chất lượng hình ảnh mặc định"), ("Default Codec", "Codec mặc định"), - ("Bitrate", "T"), - ("FPS", ""), + ("Bitrate", "Bitrate"), + ("FPS", "FPS"), ("Auto", "Tự động"), ("Other Default Options", "Các tùy chọn mặc định khác"), - ("Voice call", "Gọi âm thanh"), - ("Text chat", "Tin nhắn"), - ("Stop voice call", "Dừng cuộc gọi"), - ("relay_hint_tip", "Việc kết nối trực tiếp có thể không khả thi, bạn có thể thử kết nối qua máy chủ chuyển tiếp. \nThêm vào đó, nếu bạn muốn sử dụng máy chủ chuyển tiếp trong lần thử đầu tiên, bạn có thể thêm hậu tố \"/r\" vào sau ID, hoặc chọn tùy chọn \"Luôn kết nối qua máy chủ chuyển tiếp\""), + ("Voice call", "Gọi thoại"), + ("Text chat", "Chat văn bản"), + ("Stop voice call", "Dừng gọi thoại"), + ("relay_hint_tip", "Nếu không kết nối trực tiếp được, hãy thử qua máy chủ chuyển tiếp (ID/r)."), ("Reconnect", "Kết nối lại"), - ("Codec", ""), + ("Codec", "Codec"), ("Resolution", "Độ phân giải"), - ("No transfers in progress", "Không có tệp tin nào đang được truyền"), - ("Set one-time password length", "Thiết lập độ dài mật khẩu một lần"), + ("No transfers in progress", "Không có tệp nào đang truyền"), + ("Set one-time password length", "Đặt độ dài mật khẩu một lần"), ("RDP Settings", "Cài đặt RDP"), ("Sort by", "Sắp xếp theo"), ("New Connection", "Kết nối mới"), @@ -455,287 +455,287 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Minimize", "Thu nhỏ"), ("Maximize", "Phóng to"), ("Your Device", "Thiết bị của bạn"), - ("empty_recent_tip", "Oops, không có kết nối nào gần đây!\nĐã đến lúc kết nối rồi."), - ("empty_favorite_tip", "Chưa có người dùng yêu thích nào cả?\nHãy tìm ai đó để kết nối cùng và thêm họ vào danh sách yêu thích!"), - ("empty_lan_tip", "Ôi không, có vẻ như chúng ta chưa phát hiện ra bất cứ người dùng nào cả."), - ("empty_address_book_tip", "Ôi bạn ơi, có vẻ như bạn chưa thêm ai vào quyển địa chỉ cả."), - ("Empty Username", "Tên tài khoản trống"), + ("empty_recent_tip", "Chưa có kết nối gần đây."), + ("empty_favorite_tip", "Chưa có mục yêu thích."), + ("empty_lan_tip", "Không tìm thấy thiết bị nào trong LAN."), + ("empty_address_book_tip", "Sổ địa chỉ đang trống."), + ("Empty Username", "Tên người dùng trống"), ("Empty Password", "Mật khẩu trống"), ("Me", "Tôi"), - ("identical_file_tip", "Tệp tin này giống hệt với tệp tin của người dùng từ xa"), - ("show_monitors_tip", "Hiện các màn hình trong thanh công cụ"), + ("identical_file_tip", "Tệp này giống hệt ở phía đối tác."), + ("show_monitors_tip", "Hiện màn hình trên thanh công cụ"), ("View Mode", "Chế độ xem"), - ("login_linux_tip", "Bạn cần đăng nhập vào tài khoản Linux từ xa để bật X phiên kết nối"), + ("login_linux_tip", "Cần đăng nhập tài khoản Linux để kích hoạt X session."), ("verify_rustdesk_password_tip", "Xác thực mật khẩu RustDesk"), ("remember_account_tip", "Nhớ tài khoản này"), - ("os_account_desk_tip", "Tài khoản này đã được dùng để đăng nhập tới hệ điều hành từ xa và kích hoạt phiên kết nối ở chế độ headless"), - ("OS Account", "Tài khoản hệ điều hành"), - ("another_user_login_title_tip", "Có người dùng khác đã đăng nhập"), - ("another_user_login_text_tip", "Ngắt kết nối"), + ("os_account_desk_tip", "Tài khoản OS được dùng để đăng nhập và chạy session không màn hình (headless)."), + ("OS Account", "Tài khoản OS"), + ("another_user_login_title_tip", "Người dùng khác đã đăng nhập"), + ("another_user_login_text_tip", "Ngắt kết nối hiện tại"), ("xorg_not_found_title_tip", "Không tìm thấy Xorg"), ("xorg_not_found_text_tip", "Vui lòng cài đặt Xorg"), - ("no_desktop_title_tip", "Không có desktop khả dụng"), - ("no_desktop_text_tip", "Vui lòng cài đặt desktop GNOME"), - ("No need to elevate", "Không cần phải cấp quyền"), + ("no_desktop_title_tip", "Không có desktop"), + ("no_desktop_text_tip", "Vui lòng cài đặt GNOME hoặc desktop khác."), + ("No need to elevate", "Không cần nâng quyền"), ("System Sound", "Âm thanh hệ thống"), ("Default", "Mặc định"), ("New RDP", "RDP mới"), - ("Fingerprint", ""), + ("Fingerprint", "Dấu vân tay"), ("Copy Fingerprint", "Sao chép fingerprint"), - ("no fingerprints", "không có fingerprints"), - ("Select a peer", "Chọn một người dùng"), - ("Select peers", "Chọn nhiều người dùng"), - ("Plugins", "Tiện ích"), + ("no fingerprints", "không có fingerprint"), + ("Select a peer", "Chọn một đối tác"), + ("Select peers", "Chọn các đối tác"), + ("Plugins", "Plugin"), ("Uninstall", "Gỡ cài đặt"), ("Update", "Cập nhật"), ("Enable", "Bật"), ("Disable", "Tắt"), ("Options", "Tùy chọn"), ("resolution_original_tip", "Độ phân giải gốc"), - ("resolution_fit_local_tip", "Vừa với độ phân giải cục bộ"), + ("resolution_fit_local_tip", "Vừa với máy cục bộ"), ("resolution_custom_tip", "Độ phân giải tùy chỉnh"), - ("Collapse toolbar", "Thu nhỏ thanh công cụ"), - ("Accept and Elevate", "Chấp nhận và Cấp Quyền"), - ("accept_and_elevate_btn_tooltip", "Chấp nhận kết nối và cấp các quyền UAC."), - ("clipboard_wait_response_timeout_tip", ""), + ("Collapse toolbar", "Thu gọn thanh công cụ"), + ("Accept and Elevate", "Chấp nhận và Nâng quyền"), + ("accept_and_elevate_btn_tooltip", "Chấp nhận kết nối và nâng quyền UAC."), + ("clipboard_wait_response_timeout_tip", "Hết thời gian chờ Clipboard phản hồi."), ("Incoming connection", "Kết nối đến"), ("Outgoing connection", "Kết nối đi"), ("Exit", "Thoát"), ("Open", "Mở"), - ("logout_tip", ""), + ("logout_tip", "Bạn có chắc muốn đăng xuất?"), ("Service", "Dịch vụ"), ("Start", "Bắt đầu"), - ("Stop", "Dừng lại"), - ("exceed_max_devices", ""), - ("Sync with recent sessions", "Đồng bộ với phiên gần đây"), - ("Sort tags", ""), + ("Stop", "Dừng"), + ("exceed_max_devices", "Vượt quá số lượng thiết bị tối đa."), + ("Sync with recent sessions", "Đồng bộ với các phiên gần đây"), + ("Sort tags", "Sắp xếp thẻ"), ("Open connection in new tab", "Mở kết nối trong tab mới"), - ("Move tab to new window", ""), + ("Move tab to new window", "Di chuyển tab sang cửa sổ mới"), ("Can not be empty", "Không được để trống"), - ("Already exists", "Đã tồn tại rồi"), + ("Already exists", "Đã tồn tại"), ("Change Password", "Đổi mật khẩu"), ("Refresh Password", "Làm mới mật khẩu"), - ("ID", ""), - ("Grid View", "Xem theo dạng bảng"), - ("List View", "Xem theo dạng danh sách"), + ("ID", "ID"), + ("Grid View", "Dạng lưới"), + ("List View", "Dạng danh sách"), ("Select", "Chọn"), - ("Toggle Tags", ""), - ("pull_ab_failed_tip", ""), - ("push_ab_failed_tip", ""), - ("synced_peer_readded_tip", ""), - ("Change Color", ""), - ("Primary Color", ""), - ("HSV Color", ""), - ("Installation Successful!", ""), - ("Installation failed!", ""), - ("Reverse mouse wheel", ""), - ("{} sessions", ""), - ("scam_title", ""), - ("scam_text1", ""), - ("scam_text2", ""), - ("Don't show again", ""), - ("I Agree", ""), - ("Decline", ""), - ("Timeout in minutes", ""), - ("auto_disconnect_option_tip", ""), - ("Connection failed due to inactivity", ""), - ("Check for software update on startup", ""), - ("upgrade_rustdesk_server_pro_to_{}_tip", ""), - ("pull_group_failed_tip", ""), - ("Filter by intersection", ""), - ("Remove wallpaper during incoming sessions", ""), - ("Test", ""), - ("display_is_plugged_out_msg", ""), - ("No displays", ""), - ("Open in new window", ""), - ("Show displays as individual windows", ""), - ("Use all my displays for the remote session", ""), - ("selinux_tip", ""), - ("Change view", ""), - ("Big tiles", ""), - ("Small tiles", ""), - ("List", ""), - ("Virtual display", ""), - ("Plug out all", ""), - ("True color (4:4:4)", ""), - ("Enable blocking user input", ""), - ("id_input_tip", ""), - ("privacy_mode_impl_mag_tip", ""), - ("privacy_mode_impl_virtual_display_tip", ""), - ("Enter privacy mode", ""), - ("Exit privacy mode", ""), - ("idd_not_support_under_win10_2004_tip", ""), - ("input_source_1_tip", ""), - ("input_source_2_tip", ""), - ("Swap control-command key", ""), - ("swap-left-right-mouse", ""), - ("2FA code", "Mã xác thực 2 bước"), + ("Toggle Tags", "Bật/Tắt thẻ"), + ("pull_ab_failed_tip", "Lấy sổ địa chỉ thất bại."), + ("push_ab_failed_tip", "Đồng bộ sổ địa chỉ thất bại."), + ("synced_peer_readded_tip", "Thiết bị đã đồng bộ được thêm lại."), + ("Change Color", "Đổi màu"), + ("Primary Color", "Màu chính"), + ("HSV Color", "Màu HSV"), + ("Installation Successful!", "Cài đặt thành công!"), + ("Installation failed!", "Cài đặt thất bại!"), + ("Reverse mouse wheel", "Đảo ngược con lăn chuột"), + ("{} sessions", "{} phiên"), + ("scam_title", "CẢNH BÁO LỪA ĐẢO"), + ("scam_text1", "KHÔNG chia sẻ ID/Mật khẩu với người lạ qua điện thoại. Nếu họ yêu cầu, họ có thể là kẻ lừa đảo."), + ("scam_text2", "Chỉ sử dụng RustDesk với những người bạn thực sự tin tưởng."), + ("Don't show again", "Không hiển thị lại"), + ("I Agree", "Tôi đồng ý"), + ("Decline", "Từ chối"), + ("Timeout in minutes", "Thời gian chờ (phút)"), + ("auto_disconnect_option_tip", "Tự động ngắt kết nối khi không hoạt động"), + ("Connection failed due to inactivity", "Ngắt kết nối do không hoạt động"), + ("Check for software update on startup", "Kiểm tra cập nhật khi khởi động"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Nâng cấp lên Pro để có thêm tính năng"), + ("pull_group_failed_tip", "Lấy thông tin nhóm thất bại"), + ("Filter by intersection", "Lọc theo giao điểm"), + ("Remove wallpaper during incoming sessions", "Xóa hình nền khi có kết nối đến"), + ("Test", "Kiểm tra"), + ("display_is_plugged_out_msg", "Màn hình đã bị rút."), + ("No displays", "Không có màn hình"), + ("Open in new window", "Mở trong cửa sổ mới"), + ("Show displays as individual windows", "Hiển thị mỗi màn hình một cửa sổ"), + ("Use all my displays for the remote session", "Sử dụng tất cả màn hình của tôi"), + ("selinux_tip", "SELinux đang bật, có thể gây lỗi."), + ("Change view", "Đổi kiểu xem"), + ("Big tiles", "Ô lớn"), + ("Small tiles", "Ô nhỏ"), + ("List", "Danh sách"), + ("Virtual display", "Màn hình ảo"), + ("Plug out all", "Rút tất cả"), + ("True color (4:4:4)", "Màu thực (4:4:4)"), + ("Enable blocking user input", "Cho phép chặn đầu vào người dùng"), + ("id_input_tip", "Nhập ID hoặc IP."), + ("privacy_mode_impl_mag_tip", "Chế độ riêng tư (Magnifier)"), + ("privacy_mode_impl_virtual_display_tip", "Chế độ riêng tư (Virtual Display)"), + ("Enter privacy mode", "Vào chế độ riêng tư"), + ("Exit privacy mode", "Thoát chế độ riêng tư"), + ("idd_not_support_under_win10_2004_tip", "Yêu cầu Windows 10 2004 trở lên."), + ("input_source_1_tip", "Nguồn đầu vào 1"), + ("input_source_2_tip", "Nguồn đầu vào 2"), + ("Swap control-command key", "Hoán đổi phím Ctrl-Cmd"), + ("swap-left-right-mouse", "Hoán đổi chuột trái-phải"), + ("2FA code", "Mã 2FA"), ("More", "Thêm"), - ("enable-2fa-title", ""), - ("enable-2fa-desc", ""), - ("wrong-2fa-code", ""), - ("enter-2fa-title", ""), - ("Email verification code must be 6 characters.", "Mã xác thực email phải có 6 chữ số"), - ("2FA code must be 6 digits.", "Mã xác thực 2 bước phải có 6 chữ số"), - ("Multiple Windows sessions found", ""), - ("Please select the session you want to connect to", ""), - ("powered_by_me", ""), - ("outgoing_only_desk_tip", ""), - ("preset_password_warning", ""), + ("enable-2fa-title", "Bật xác thực 2 bước"), + ("enable-2fa-desc", "Vui lòng quét mã QR để bật 2FA."), + ("wrong-2fa-code", "Mã 2FA sai"), + ("enter-2fa-title", "Nhập mã 2FA"), + ("Email verification code must be 6 characters.", "Mã xác thực email phải có 6 ký tự."), + ("2FA code must be 6 digits.", "Mã 2FA phải có 6 chữ số."), + ("Multiple Windows sessions found", "Tìm thấy nhiều phiên Windows"), + ("Please select the session you want to connect to", "Chọn phiên bạn muốn kết nối"), + ("powered_by_me", "Cung cấp bởi tôi"), + ("outgoing_only_desk_tip", "Chỉ cho phép kết nối đi."), + ("preset_password_warning", "Cảnh báo mật khẩu thiết lập sẵn"), ("Security Alert", "Cảnh báo bảo mật"), - ("My address book", ""), + ("My address book", "Sổ địa chỉ của tôi"), ("Personal", "Cá nhân"), - ("Owner", "Chủ"), - ("Set shared password", "Cài đặt mật khẩu được chia sẻ"), + ("Owner", "Chủ sở hữu"), + ("Set shared password", "Đặt mật khẩu chia sẻ"), ("Exist in", "Tồn tại trong"), - ("Read-only", "Chỉ-đọc"), + ("Read-only", "Chỉ đọc"), ("Read/Write", "Đọc/Ghi"), ("Full Control", "Toàn quyền"), - ("share_warning_tip", ""), + ("share_warning_tip", "Cẩn thận khi chia sẻ quyền điều khiển!"), ("Everyone", "Mọi người"), - ("ab_web_console_tip", ""), - ("allow-only-conn-window-open-tip", ""), - ("no_need_privacy_mode_no_physical_displays_tip", ""), - ("Follow remote cursor", ""), - ("Follow remote window focus", ""), - ("default_proxy_tip", ""), - ("no_audio_input_device_tip", ""), - ("Incoming", ""), - ("Outgoing", ""), - ("Clear Wayland screen selection", ""), - ("clear_Wayland_screen_selection_tip", ""), - ("confirm_clear_Wayland_screen_selection_tip", ""), - ("android_new_voice_call_tip", ""), - ("texture_render_tip", ""), - ("Use texture rendering", ""), - ("Floating window", ""), - ("floating_window_tip", ""), - ("Keep screen on", "Giữ màn hình bật"), + ("ab_web_console_tip", "Quản lý qua Web Console"), + ("allow-only-conn-window-open-tip", "Chỉ cho phép khi cửa sổ RustDesk mở"), + ("no_need_privacy_mode_no_physical_displays_tip", "Không cần chế độ riêng tư vì không có màn hình vật lý."), + ("Follow remote cursor", "Theo con trỏ từ xa"), + ("Follow remote window focus", "Theo tiêu điểm cửa sổ từ xa"), + ("default_proxy_tip", "Proxy mặc định"), + ("no_audio_input_device_tip", "Không tìm thấy thiết bị thu âm."), + ("Incoming", "Đang đến"), + ("Outgoing", "Đang đi"), + ("Clear Wayland screen selection", "Xóa lựa chọn màn hình Wayland"), + ("clear_Wayland_screen_selection_tip", "Đặt lại các quyền chọn màn hình."), + ("confirm_clear_Wayland_screen_selection_tip", "Bạn có chắc muốn đặt lại?"), + ("android_new_voice_call_tip", "Yêu cầu gọi thoại mới."), + ("texture_render_tip", "Sử dụng Texture Rendering"), + ("Use texture rendering", "Sử dụng Texture Rendering"), + ("Floating window", "Cửa sổ nổi"), + ("floating_window_tip", "Giữ RustDesk trên cùng"), + ("Keep screen on", "Giữ màn hình luôn bật"), ("Never", "Không bao giờ"), - ("During controlled", "Trong khi được điều khiển"), - ("During service is on", "Trong khi dịch vụ được bật"), - ("Capture screen using DirectX", "Chụp màn hình với DirectX"), + ("During controlled", "Trong khi bị điều khiển"), + ("During service is on", "Trong khi dịch vụ đang bật"), + ("Capture screen using DirectX", "Chụp màn hình bằng DirectX"), ("Back", "Trở về"), ("Apps", "Ứng dụng"), ("Volume up", "Tăng âm lượng"), ("Volume down", "Giảm âm lượng"), ("Power", "Nguồn"), - ("Telegram bot", ""), - ("enable-bot-tip", ""), - ("enable-bot-desc", ""), - ("cancel-2fa-confirm-tip", ""), - ("cancel-bot-confirm-tip", ""), - ("About RustDesk", "Về RuskDest"), - ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), - ("Unlock with PIN", "Mở khóa với mã PIN"), - ("Requires at least {} characters", ""), - ("Wrong PIN", "Sai mã PIN"), + ("Telegram bot", "Telegram Bot"), + ("enable-bot-tip", "Bật thông báo qua Telegram"), + ("enable-bot-desc", "Liên kết với Telegram Bot của bạn."), + ("cancel-2fa-confirm-tip", "Xác nhận tắt 2FA?"), + ("cancel-bot-confirm-tip", "Xác nhận tắt Bot?"), + ("About RustDesk", "Về RustDesk"), + ("Send clipboard keystrokes", "Gửi phím từ Clipboard"), + ("network_error_tip", "Lỗi mạng, vui lòng kiểm tra lại."), + ("Unlock with PIN", "Mở khóa bằng mã PIN"), + ("Requires at least {} characters", "Yêu cầu ít nhất {} ký tự"), + ("Wrong PIN", "Mã PIN sai"), ("Set PIN", "Đặt mã PIN"), - ("Enable trusted devices", "Kích hoạt thiết bị tin cậy"), + ("Enable trusted devices", "Bật thiết bị tin cậy"), ("Manage trusted devices", "Quản lý thiết bị tin cậy"), ("Platform", "Nền tảng"), ("Days remaining", "Số ngày còn lại"), - ("enable-trusted-devices-tip", ""), + ("enable-trusted-devices-tip", "Chỉ thiết bị tin cậy mới có thể kết nối không cần mật khẩu."), ("Parent directory", "Thư mục cha"), ("Resume", "Tiếp tục"), ("Invalid file name", "Tên tệp không hợp lệ"), - ("one-way-file-transfer-tip", ""), + ("one-way-file-transfer-tip", "Chỉ cho phép truyền tệp một chiều."), ("Authentication Required", "Yêu cầu xác thực"), - ("Authenticate", ""), - ("web_id_input_tip", ""), - ("Download", ""), - ("Upload folder", ""), - ("Upload files", ""), - ("Clipboard is synchronized", ""), - ("Update client clipboard", ""), - ("Untagged", ""), - ("new-version-of-{}-tip", ""), - ("Accessible devices", ""), - ("upgrade_remote_rustdesk_client_to_{}_tip", ""), - ("d3d_render_tip", ""), - ("Use D3D rendering", ""), - ("Printer", ""), - ("printer-os-requirement-tip", ""), - ("printer-requires-installed-{}-client-tip", ""), - ("printer-{}-not-installed-tip", ""), - ("printer-{}-ready-tip", ""), - ("Install {} Printer", ""), - ("Outgoing Print Jobs", ""), - ("Incoming Print Jobs", ""), - ("Incoming Print Job", ""), - ("use-the-default-printer-tip", ""), - ("use-the-selected-printer-tip", ""), - ("auto-print-tip", ""), - ("print-incoming-job-confirm-tip", ""), - ("remote-printing-disallowed-tile-tip", ""), - ("remote-printing-disallowed-text-tip", ""), - ("save-settings-tip", ""), - ("dont-show-again-tip", ""), + ("Authenticate", "Xác thực"), + ("web_id_input_tip", "Nhập ID để bắt đầu kết nối Web."), + ("Download", "Tải xuống"), + ("Upload folder", "Tải lên thư mục"), + ("Upload files", "Tải lên tệp"), + ("Clipboard is synchronized", "Clipboard đã được đồng bộ"), + ("Update client clipboard", "Cập nhật Clipboard của khách"), + ("Untagged", "Chưa gắn thẻ"), + ("new-version-of-{}-tip", "Đã có phiên bản mới của {}"), + ("Accessible devices", "Thiết bị có thể truy cập"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Vui lòng nâng cấp đối tác lên {}"), + ("d3d_render_tip", "Sử dụng D3D Rendering"), + ("Use D3D rendering", "Sử dụng D3D Rendering"), + ("Printer", "Máy in"), + ("printer-os-requirement-tip", "Yêu cầu hệ điều hành hỗ trợ máy in."), + ("printer-requires-installed-{}-client-tip", "Cần cài đặt driver {}"), + ("printer-{}-not-installed-tip", "Máy in {} chưa được cài đặt."), + ("printer-{}-ready-tip", "Máy in {} đã sẵn sàng."), + ("Install {} Printer", "Cài đặt máy in {}"), + ("Outgoing Print Jobs", "Yêu cầu in đi"), + ("Incoming Print Jobs", "Yêu cầu in đến"), + ("Incoming Print Job", "Yêu cầu in đến"), + ("use-the-default-printer-tip", "Sử dụng máy in mặc định"), + ("use-the-selected-printer-tip", "Sử dụng máy in đã chọn"), + ("auto-print-tip", "Tự động in"), + ("print-incoming-job-confirm-tip", "Xác nhận in tệp này?"), + ("remote-printing-disallowed-tile-tip", "In từ xa bị cấm"), + ("remote-printing-disallowed-text-tip", "Vui lòng bật quyền in trong cài đặt."), + ("save-settings-tip", "Lưu cài đặt"), + ("dont-show-again-tip", "Đừng hiện lại"), ("Take screenshot", "Chụp màn hình"), - ("Taking screenshot", "Đang chụp màn hình"), - ("screenshot-merged-screen-not-supported-tip", ""), - ("screenshot-action-tip", ""), + ("Taking screenshot", "Đang chụp màn hình..."), + ("screenshot-merged-screen-not-supported-tip", "Không hỗ trợ chụp gộp nhiều màn hình."), + ("screenshot-action-tip", "Hành động chụp màn hình"), ("Save as", "Lưu thành"), - ("Copy to clipboard", "Sao chép vào bảng nhớ"), - ("Enable remote printer", "Kích hoat máy in ở xa"), - ("Downloading {}", "Đang tải xuống"), - ("{} Update", ""), - ("{}-to-update-tip", ""), - ("download-new-version-failed-tip", ""), + ("Copy to clipboard", "Sao chép vào Clipboard"), + ("Enable remote printer", "Bật máy in từ xa"), + ("Downloading {}", "Đang tải xuống {}"), + ("{} Update", "Cập nhật {}"), + ("{}-to-update-tip", "Cần nâng cấp để sử dụng tính năng này."), + ("download-new-version-failed-tip", "Tải phiên bản mới thất bại."), ("Auto update", "Tự động cập nhật"), - ("update-failed-check-msi-tip", ""), - ("websocket_tip", ""), + ("update-failed-check-msi-tip", "Cập nhật lỗi, vui lòng kiểm tra file MSI."), + ("websocket_tip", "Sử dụng giao thức WebSocket"), ("Use WebSocket", "Sử dụng WebSocket"), - ("Trackpad speed", "Tốc độ trackpad"), - ("Default trackpad speed", "Tốc độ trackpad mặc định"), + ("Trackpad speed", "Tốc độ Trackpad"), + ("Default trackpad speed", "Tốc độ Trackpad mặc định"), ("Numeric one-time password", "Mật khẩu số dùng một lần"), - ("Enable IPv6 P2P connection", "Cho phép kết nốt IPv6 P2P"), - ("Enable UDP hole punching", ""), - ("View camera", "Xem camera"), - ("Enable camera", "Kích hoạt máy ảnh"), - ("No cameras", "Không có máy ảnh"), - ("view_camera_unsupported_tip", ""), - ("Terminal", "Bảng điều khiển"), - ("Enable terminal", "Kích hoạt bảng điều khiển"), + ("Enable IPv6 P2P connection", "Cho phép kết nối IPv6 P2P"), + ("Enable UDP hole punching", "Bật UDP Hole Punching"), + ("View camera", "Xem Camera"), + ("Enable camera", "Bật Camera"), + ("No cameras", "Không có camera"), + ("view_camera_unsupported_tip", "Đối tác chưa hỗ trợ xem camera."), + ("Terminal", "Terminal"), + ("Enable terminal", "Bật Terminal"), ("New tab", "Tab mới"), - ("Keep terminal sessions on disconnect", "Giữ các phiên của bảng điều khiển ngắt kết nối"), - ("Terminal (Run as administrator)", "Bảng điều khiển (Chạy với quyền quản trị viên)"), - ("terminal-admin-login-tip", ""), - ("Failed to get user token.", "Thất bại trong việc lấy token của người dùng"), - ("Incorrect username or password.", "Tên người dùng hoặc mật khẩu không chính xác."), - ("The user is not an administrator.", "Người dùng không phải là quản trị viên."), - ("Failed to check if the user is an administrator.", "Thất bại trong việc kiểm tra người dùng là quản trị viên."), - ("Supported only in the installed version.", "Chỉ hỗ trợ phiên bản đã được cài đặt."), - ("elevation_username_tip", ""), - ("Preparing for installation ...", "Đang chuẩn bị để cài đặt ..."), - ("Show my cursor", "Hiện con trỏ"), - ("Scale custom", "Tùy chỉnh "), - ("Custom scale slider", ""), + ("Keep terminal sessions on disconnect", "Giữ phiên terminal khi ngắt kết nối"), + ("Terminal (Run as administrator)", "Terminal (Quyền Quản trị viên)"), + ("terminal-admin-login-tip", "Đang đăng nhập quyền quản trị..."), + ("Failed to get user token.", "Lấy mã token người dùng thất bại."), + ("Incorrect username or password.", "Tên người dùng hoặc mật khẩu sai."), + ("The user is not an administrator.", "Người dùng không phải Quản trị viên."), + ("Failed to check if the user is an administrator.", "Kiểm tra quyền Quản trị viên thất bại."), + ("Supported only in the installed version.", "Chỉ hỗ trợ trên bản đã cài đặt."), + ("elevation_username_tip", "Tên đăng nhập để nâng quyền"), + ("Preparing for installation ...", "Đang chuẩn bị cài đặt..."), + ("Show my cursor", "Hiện con trỏ của tôi"), + ("Scale custom", "Tùy chỉnh tỷ lệ"), + ("Custom scale slider", "Thanh trượt tỷ lệ"), ("Decrease", "Giảm"), ("Increase", "Tăng"), ("Show virtual mouse", "Hiện chuột ảo"), ("Virtual mouse size", "Kích thước chuột ảo"), ("Small", "Nhỏ"), ("Large", "Lớn"), - ("Show virtual joystick", "Hiện nút điều khiển ảo"), - ("Edit note", "Sửa ghi chép"), - ("Alias", "Ánh xạ"), - ("ScrollEdge", ""), - ("Allow insecure TLS fallback", ""), - ("allow-insecure-tls-fallback-tip", ""), - ("Disable UDP", "Ngắt kết nối UDP"), - ("disable-udp-tip", ""), - ("server-oss-not-support-tip", ""), - ("input note here", ""), - ("note-at-conn-end-tip", ""), - ("Show terminal extra keys", ""), - ("Relative mouse mode", ""), - ("rel-mouse-not-supported-peer-tip", ""), - ("rel-mouse-not-ready-tip", ""), - ("rel-mouse-lock-failed-tip", ""), - ("rel-mouse-exit-{}-tip", ""), - ("rel-mouse-permission-lost-tip", ""), - ("Changelog", ""), + ("Show virtual joystick", "Hiện Joystick ảo"), + ("Edit note", "Sửa ghi chú"), + ("Alias", "Bí danh"), + ("ScrollEdge", "Cuộn ở cạnh"), + ("Allow insecure TLS fallback", "Cho phép hạ cấp TLS không an toàn"), + ("allow-insecure-tls-fallback-tip", "Cho phép kết nối nếu máy chủ dùng TLS cũ."), + ("Disable UDP", "Tắt UDP"), + ("disable-udp-tip", "Chỉ sử dụng TCP để kết nối."), + ("server-oss-not-support-tip", "Máy chủ mã nguồn mở không hỗ trợ tính năng này."), + ("input note here", "nhập ghi chú tại đây"), + ("note-at-conn-end-tip", "Hiện ghi chú khi kết thúc phiên"), + ("Show terminal extra keys", "Hiện các phím phụ Terminal"), + ("Relative mouse mode", "Chế độ chuột tương đối"), + ("rel-mouse-not-supported-peer-tip", "Đối tác không hỗ trợ chuột tương đối."), + ("rel-mouse-not-ready-tip", "Chuột tương đối chưa sẵn sàng."), + ("rel-mouse-lock-failed-tip", "Khóa chuột thất bại."), + ("rel-mouse-exit-{}-tip", "Thoát chế độ chuột tương đối: {}"), + ("rel-mouse-permission-lost-tip", "Mất quyền điều khiển chuột tương đối."), + ("Changelog", "Nhật ký thay đổi"), ].iter().cloned().collect(); } From 7276025cf9fb163a9958c5227c6bf6e22868cab1 Mon Sep 17 00:00:00 2001 From: Kratos Date: Tue, 13 Jan 2026 04:00:29 +0100 Subject: [PATCH 082/277] Update hu.rs (#14032) Fix translated strings. --- src/lang/hu.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lang/hu.rs b/src/lang/hu.rs index d9300bae6..d7b82ff7a 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -730,12 +730,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", "Megjegyzés beírása"), ("note-at-conn-end-tip", "Kérjen megjegyzést a kapcsolat végén"), ("Show terminal extra keys", "További terminálgombok megjelenítése"), - ("Relative mouse mode", "Relatív egér mód"), - ("rel-mouse-not-supported-peer-tip", "A célkészülék nem támogatja a relatív egér módot."), - ("rel-mouse-not-ready-tip", "A relatív egér mód még nem áll készen. Kérjük, próbálkozzon később újra!"), - ("rel-mouse-lock-failed-tip", "Az egér nem zárolható, a relatív egér mód le van tiltva."), - ("rel-mouse-exit-{}-tip", "A kilépéshez nyomja meg a {} gombot."), - ("rel-mouse-permission-lost-tip", "A billentyűzet engedélyei visszavonásra kerültek. A relatív egér mód letiltásra került."), - ("Changelog", "Változásnapló"), + ("Relative mouse mode", "Relatív egérmód"), + ("rel-mouse-not-supported-peer-tip", "A kapcsolódott partner nem támogatja a relatív egérmódot."), + ("rel-mouse-not-ready-tip", "A relatív egérmód még nem elérhető. Próbálja meg újra."), + ("rel-mouse-lock-failed-tip", "Nem sikerült zárolni a kurzort. A relatív egérmód le lett tiltva."), + ("rel-mouse-exit-{}-tip", "A kilépéshez nyomja meg a(z) {} gombot."), + ("rel-mouse-permission-lost-tip", "A billentyűzet-hozzáférés vissza lett vonva. A relatív egérmód le lett tilva."), + ("Changelog", "Változáslista"), ].iter().cloned().collect(); } From 92ad279324fc9adee12ec09ed01f7e23964cbc54 Mon Sep 17 00:00:00 2001 From: Alex Rijckaert Date: Wed, 14 Jan 2026 06:05:01 +0100 Subject: [PATCH 083/277] Dutch Translation up to date (#14033) --- src/lang/nl.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 6b2c7dc66..2c3400dc8 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -730,12 +730,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", "voeg hier een opmerking toe"), ("note-at-conn-end-tip", "Vraag om een opmerking aan het einde van de verbinding"), ("Show terminal extra keys", "Toon extra toetsen voor terminal"), - ("Relative mouse mode", ""), - ("rel-mouse-not-supported-peer-tip", ""), - ("rel-mouse-not-ready-tip", ""), - ("rel-mouse-lock-failed-tip", ""), - ("rel-mouse-exit-{}-tip", ""), - ("rel-mouse-permission-lost-tip", ""), - ("Changelog", ""), + ("Relative mouse mode", "Relatieve muismodus"), + ("rel-mouse-not-supported-peer-tip", "De relatieve muismodus wordt niet ondersteund door het externe apparaat."), + ("rel-mouse-not-ready-tip", "De relatieve muismodus was nog niet klaar, probeer het later opnieuw."), + ("rel-mouse-lock-failed-tip", "Het vergrendelen van de cursor is mislukt. De relatieve muismodus is uitgeschakeld."), + ("rel-mouse-exit-{}-tip", "Druk op {} om af te sluiten."), + ("rel-mouse-permission-lost-tip", "De toetsenbordcontrole is uitgeschakeld. De relatieve muismodus is uitgeschakeld."), + ("Changelog", "Wijzigingenlogboek"), ].iter().cloned().collect(); } From c4a9835ae539dc566b7882ea53b5169680517fba Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 15 Jan 2026 13:47:39 +0800 Subject: [PATCH 084/277] change quick support filename detection (#14050) Signed-off-by: 21pages --- libs/portable/src/main.rs | 10 +++++++++- src/core_main.rs | 11 ++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/libs/portable/src/main.rs b/libs/portable/src/main.rs index 85b19e9e9..1c754cc74 100644 --- a/libs/portable/src/main.rs +++ b/libs/portable/src/main.rs @@ -187,7 +187,7 @@ fn main() { i += 1; } let click_setup = args.is_empty() && arg_exe.to_lowercase().ends_with("install.exe"); - let quick_support = args.is_empty() && arg_exe.to_lowercase().ends_with("qs.exe"); + let quick_support = args.is_empty() && win::is_quick_support_exe(&arg_exe); let mut ui = false; let reader = BinaryReader::default(); @@ -234,4 +234,12 @@ mod win { .output(); let _allow_err = std::fs::copy(src, &format!("{}\\{}", dir.to_string_lossy(), tgt)); } + + /// Check if the executable is a Quick Support version. + /// Note: This function must be kept in sync with `src/core_main.rs`. + #[inline] + pub(super) fn is_quick_support_exe(exe: &str) -> bool { + let exe = exe.to_lowercase(); + exe.contains("-qs-") || exe.contains("-qs.exe") || exe.contains("_qs.exe") + } } diff --git a/src/core_main.rs b/src/core_main.rs index ad8154dc6..7962a693e 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -140,7 +140,7 @@ pub fn core_main() -> Option> { { _is_quick_support |= !crate::platform::is_installed() && args.is_empty() - && (arg_exe.to_lowercase().contains("-qs-") + && (is_quick_support_exe(&arg_exe) || config::LocalConfig::get_option("pre-elevate-service") == "Y" || (!click_setup && crate::platform::is_elevated(None).unwrap_or(false))); crate::portable_service::client::set_quick_support(_is_quick_support); @@ -829,3 +829,12 @@ fn is_root() -> bool { #[allow(unreachable_code)] crate::platform::is_root() } + +/// Check if the executable is a Quick Support version. +/// Note: This function must be kept in sync with `libs/portable/src/main.rs`. +#[cfg(windows)] +#[inline] +fn is_quick_support_exe(exe: &str) -> bool { + let exe = exe.to_lowercase(); + exe.contains("-qs-") || exe.contains("-qs.exe") || exe.contains("_qs.exe") +} From a2243484a3bd0a513dbdc7d1541bcbe2bbf4a8dd Mon Sep 17 00:00:00 2001 From: hatterp Date: Sat, 17 Jan 2026 11:31:41 +0100 Subject: [PATCH 085/277] Update README-PL.md (#14052) --- docs/README-PL.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/README-PL.md b/docs/README-PL.md index 2cb4123ea..437682a9c 100644 --- a/docs/README-PL.md +++ b/docs/README-PL.md @@ -13,7 +13,9 @@ Porozmawiaj z nami na: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](http [![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Zaawansowane%20Funkcje-blue)](https://rustdesk.com/pricing.html) -Kolejny program do zdalnego pulpitu, napisany w Rust. Działa od samego początku, nie wymaga konfiguracji. Masz pełną kontrolę nad swoimi danymi, bez obaw o bezpieczeństwo. Możesz skorzystać z naszego darmowego serwera publicznego, [skonfigurować własny](https://rustdesk.com/server), lub [napisać własny serwer](https://github.com/rustdesk/rustdesk-server-demo). +## O projekcie + +RustDesk to wieloplatformowe oprogramowanie do zdalnego pulpitu, napisane w języku Rust, zaprojektowane z myślą o prostocie wdrożenia, bezpieczeństwie i pełnej kontroli użytkownika nad danymi. Aplikacja działa od razu po uruchomieniu i nie wymaga skomplikowanej konfiguracji. Możesz skorzystać z naszego darmowego serwera publicznego, [skonfigurować własny](https://rustdesk.com/server), lub [napisać własny serwer](https://github.com/rustdesk/rustdesk-server-demo). ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) @@ -31,7 +33,7 @@ RustDesk zaprasza do współpracy każdego. Zobacz [`docs/CONTRIBUTING-PL.md`](C ## Zależności -Wersje desktopowe używają [sciter](https://sciter.com/) dla GUI, proszę pobrać samodzielnie bibliotekę sciter. +Wersje desktopowe korzystają z biblioteki [sciter](https://sciter.com/) jako silnika GUI. Bibliotekę Sciter należy pobrać i zainstalować samodzielnie. [Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | [Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | From b9ebddff0c59c6904892df41a6db395e7ef8b466 Mon Sep 17 00:00:00 2001 From: hatterp Date: Sun, 18 Jan 2026 12:34:26 +0100 Subject: [PATCH 086/277] Update pl.rs (#14053) Add and improve Polish translation. --- src/lang/pl.rs | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 2bae03e2d..3198ba868 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -13,7 +13,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Service is running", "Usługa uruchomiona"), ("Service is not running", "Usługa nie jest uruchomiona"), ("not_ready_status", "Brak gotowości"), - ("Control Remote Desktop", "Połącz się z"), + ("Control Remote Desktop", "Steruj pulpitem zdalnym"), ("Transfer file", "Transfer plików"), ("Connect", "Połącz"), ("Recent sessions", "Ostatnie sesje"), @@ -75,7 +75,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Do you want to enter again?", "Czy chcesz wprowadzić ponownie?"), ("Connection Error", "Błąd połączenia"), ("Error", "Błąd"), - ("Reset by the peer", "Połączenie zresetowanie przez zdalne urządzenie"), + ("Reset by the peer", "Połączenie zresetowane przez zdalne urządzenie"), ("Connecting...", "Łączenie..."), ("Connection in progress. Please wait.", "Trwa łączenie. Proszę czekać."), ("Please try 1 minute later", "Spróbuj za minutę"), @@ -120,7 +120,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Oryginalny"), ("Shrink", "Zmniejsz"), ("Stretch", "Rozciągnij"), - ("Scrollbar", "Przewijanie ręczne"), + ("Scrollbar", "Pasek przewijania"), ("ScrollAuto", "Przewijanie automatyczne"), ("Good image quality", "Wysoka jakość obrazu"), ("Balanced", "Tryb zbalansowany"), @@ -161,7 +161,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("End-user license agreement", "Umowa licencyjna użytkownika końcowego"), ("Generating ...", "Trwa generowanie..."), ("Your installation is lower version.", "Twoja instalacja jest w niższej wersji"), - ("not_close_tcp_tip", "Podczas korzystanie z tunelowania, nie zamykaj tego okna."), + ("not_close_tcp_tip", "Podczas korzystania z tunelowania, nie zamykaj tego okna."), ("Listening ...", "Nasłuchiwanie..."), ("Remote Host", "Host zdalny"), ("Remote Port", "Port zdalny"), @@ -198,7 +198,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Fix it", "Napraw to"), ("Warning", "Ostrzeżenie"), ("Login screen using Wayland is not supported", "Ekran logowania korzystający z Wayland nie jest obsługiwany"), - ("Reboot required", "Wymagany ponowne uruchomienie"), + ("Reboot required", "Wymagane ponowne uruchomienie"), ("Unsupported display server", "Nieobsługiwany serwer wyświetlania"), ("x11 expected", "Wymagany jest X11"), ("Port", "Port"), @@ -225,7 +225,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add Tag", "Dodaj Tag"), ("Unselect all tags", "Odznacz wszystkie tagi"), ("Network error", "Błąd sieci"), - ("Username missed", "Nieprawidłowe nazwa użytkownika"), + ("Username missed", "Nieprawidłowa nazwa użytkownika"), ("Password missed", "Nieprawidłowe hasło"), ("Wrong credentials", "Błędne dane uwierzytelniające"), ("The verification code is incorrect or has expired", "Kod weryfikacyjny jest niepoprawny lub wygasł"), @@ -265,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Brak uprawnień na przesyłanie plików"), ("Note", "Notatka"), ("Connection", "Połączenie"), - ("Share screen", "Udostępnij ekran"), + ("Share screen", "Udostępnianie ekranu"), ("Chat", "Czat"), ("Total", "Łącznie"), ("items", "elementów"), @@ -314,10 +314,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable remote restart", "Włącz zdalne restartowanie"), ("Restart remote device", "Zrestartuj zdalne urządzenie"), ("Are you sure you want to restart", "Czy na pewno uruchomić ponownie"), - ("Restarting remote device", "Trwa restartowanie Zdalnego Urządzenia"), + ("Restarting remote device", "Trwa restartowanie zdalnego urządzenia"), ("remote_restarting_tip", "Trwa ponownie uruchomienie zdalnego urządzenia, zamknij ten komunikat i ponownie nawiąż za chwilę połączenie używając hasła permanentnego"), ("Copied", "Skopiowano"), - ("Exit Fullscreen", "Wyłączyć tryb pełnoekranowy"), + ("Exit Fullscreen", "Wyłącz tryb pełnoekranowy"), ("Fullscreen", "Tryb pełnoekranowy"), ("Mobile Actions", "Dostępne mobilne polecenia"), ("Select Monitor", "Wybierz ekran"), @@ -379,7 +379,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Share", "Udostępnianie ekranu"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland wymaga Ubuntu 21.04 lub nowszego."), ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland wymaga nowszej dystrybucji Linuksa. Wypróbuj pulpit X11 lub zmień system operacyjny."), - ("JumpLink", "View"), + ("JumpLink", "Podgląd"), ("Please Select the screen to be shared(Operate on the peer side).", "Wybierz ekran do udostępnienia (działaj po zdalnego urządzenia)."), ("Show RustDesk", "Pokaż RustDesk"), ("This PC", "Ten komputer"), @@ -403,13 +403,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to address book", "Dodaj do Książki Adresowej"), ("Group", "Grupy"), ("Search", "Szukaj"), - ("Closed manually by web console", "Zakończone manualnie z konsoli Web"), + ("Closed manually by web console", "Zakończone ręcznie z poziomu konsoli webowej"), ("Local keyboard type", "Lokalny typ klawiatury"), ("Select local keyboard type", "Wybierz lokalny typ klawiatury"), ("software_render_tip", "Jeżeli posiadasz kartę graficzną Nvidia i okno zamyka się natychmiast po nawiązaniu połączenia, instalacja sterownika nouveau i wybór renderowania programowego mogą pomóc. Restart aplikacji jest wymagany."), ("Always use software rendering", "Zawsze używaj renderowania programowego"), ("config_input", "By kontrolować zdalne urządzenie przy pomocy klawiatury, musisz udzielić aplikacji RustDesk uprawnień do \"Urządzeń Wejściowych\"."), - ("config_microphone", "Aby umożliwić zdalne rozmowy należy przyznać RuskDesk uprawnienia do \"Nagrań audio\"."), + ("config_microphone", "Aby umożliwić zdalne rozmowy należy przyznać RustDesk uprawnienia do \"Nagrań audio\"."), ("request_elevation_tip", "Możesz poprosić o podniesienie uprawnień jeżeli ktoś posiada dostęp do zdalnego urządzenia."), ("Wait", "Czekaj"), ("Elevation Error", "Błąd przy podnoszeniu uprawnień"), @@ -729,13 +729,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", "UWAGA: Serwer OSS RustDesk nie obsługuje tej funkcji."), ("input note here", "Wstaw tutaj notatkę"), ("note-at-conn-end-tip", "Poproś o notatkę po zakończeniu połączenia."), - ("Show terminal extra keys", ""), - ("Relative mouse mode", ""), - ("rel-mouse-not-supported-peer-tip", ""), - ("rel-mouse-not-ready-tip", ""), - ("rel-mouse-lock-failed-tip", ""), - ("rel-mouse-exit-{}-tip", ""), - ("rel-mouse-permission-lost-tip", ""), - ("Changelog", ""), + ("Show terminal extra keys", "Pokaż dodatkowe klawisze terminala"), + ("Relative mouse mode", "Tryb przechwytywania myszy"), + ("rel-mouse-not-supported-peer-tip", "Zdalne urządzenie nie obsługuje trybu przechwytywania myszy"), + ("rel-mouse-not-ready-tip", "Tryb przechwytywania myszy nie jest gotowy"), + ("rel-mouse-lock-failed-tip", "Nie udało się przechwycić kursora myszy"), + ("rel-mouse-exit-{}-tip", "Aby wyłączyć tryb przechwytywania myszy, naciśnij {}"), + ("rel-mouse-permission-lost-tip", "Utracono uprawnienia do trybu przechwytywania myszy"), + ("Changelog", "Dziennik zmian"), ].iter().cloned().collect(); } From b4f60e605713ecff3fe435ed781e622d13c827d7 Mon Sep 17 00:00:00 2001 From: hatterp Date: Mon, 19 Jan 2026 06:41:10 +0100 Subject: [PATCH 087/277] Update pl.rs (#14054) improve Polish translation Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> --- src/lang/pl.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 3198ba868..955d55b2c 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -459,8 +459,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Brak ulubionych?\nZnajdźmy kogoś, z kim możesz się połączyć i dodaj Go do ulubionych!"), ("empty_lan_tip", "Ojej, wygląda na to, że nie odkryliśmy żadnych urządzeń z RustDesk w Twojej sieci."), ("empty_address_book_tip", "Ojej, wygląda na to, że nie ma żadnych wpisów w Twojej książce adresowej."), - ("Empty Username", "Pusty użytkownik"), - ("Empty Password", "Puste hasło"), + ("Empty Username", "Pole nazwy użytkownika jest puste"), + ("Empty Password", "Pole hasła jest puste"), ("Me", "Ja"), ("identical_file_tip", "Ten plik jest identyczny z plikiem na drugim komputerze."), ("show_monitors_tip", "Pokaż monitory w zasobniku"), From f21829b075265fce069a95cc7eb99b9f51a6c60c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?VenusGirl=E2=9D=A4?= Date: Tue, 20 Jan 2026 18:08:02 +0900 Subject: [PATCH 088/277] Update Korean (#14057) --- src/lang/ko.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/ko.rs b/src/lang/ko.rs index c36a4ee7e..00806a0e0 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -10,7 +10,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("connecting_status", "RustDesk 네트워크에 연결 중..."), ("Enable service", "서비스 활성화"), ("Start service", "서비스 시작"), - ("Service is running", "서비스가 실행 중 입니다"), + ("Service is running", "서비스가 실행 중입니다"), ("Service is not running", "서비스가 실행되지 않았습니다"), ("not_ready_status", "준비되지 않았습니다. 연결을 확인해 주세요"), ("Control Remote Desktop", "원격 데스크탑 제어"), @@ -621,7 +621,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Volume down", "볼륨 낮추기"), ("Power", "전원"), ("Telegram bot", "Telegram 봇"), - ("enable-bot-tip", "이 기능을 활성화하면 봇에서 이중 인중 코드를 받을 수 있습니다. 또한 연결 알림 기능도 할 수 있습니다."), + ("enable-bot-tip", "이 기능을 활성화하면 봇에서 이중 인증 코드를 받을 수 있습니다. 또한 연결 알림 기능도 할 수 있습니다."), ("enable-bot-desc", "1. @BotFather와 채팅을 시작합니다.\n2. \"/newbot\" 명령을 보내주세요. 이 단계를 완료하면 토큰을 받게 됩니다.\n3. 새로 만든 봇과 채팅을 시작합니다. \"/hello\"와 같이 앞에 슬래시 (\"/\")로 시작하는 메시지를 보내 활성화합니다."), ("cancel-2fa-confirm-tip", "이중 인증을 취소하시겠습니까?"), ("cancel-bot-confirm-tip", "Telegram 봇을 취소하시겠습니까?"), @@ -736,6 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", "커서 잠금에 실패했습니다. 상대 마우스 모드가 비활성화되었습니다"), ("rel-mouse-exit-{}-tip", "종료하려면 {}을(를) 누르세요."), ("rel-mouse-permission-lost-tip", "키보드 권한이 취소되었습니다. 상대 마우스 모드가 비활성화되었습니다."), - ("Changelog", ""), + ("Changelog", ""변경 기록), ].iter().cloned().collect(); } From 7437593ee76219e6c44af6d86f269c7af421f62c Mon Sep 17 00:00:00 2001 From: Cody Kim <50035753+0-Chan@users.noreply.github.com> Date: Tue, 20 Jan 2026 18:08:54 +0900 Subject: [PATCH 089/277] Update ko.rs (#14055) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * update: correct Korean translations (typo/grammar) - typo: 인중 -> 인증 - grammar: 중 입니다 -> 중입니다 Signed-off-by: 0-Chan * update: improve Korean translations Signed-off-by: 0-Chan --------- Signed-off-by: 0-Chan --- src/lang/ko.rs | 54 +++++++++++++++++++++++++------------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 00806a0e0..1193f735e 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -17,16 +17,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Transfer file", "파일 전송"), ("Connect", "연결"), ("Recent sessions", "최근 세션"), - ("Address book", "세션 주소록"), + ("Address book", "주소록"), ("Confirmation", "확인"), ("TCP tunneling", "TCP 터널링"), ("Remove", "삭제"), ("Refresh random password", "임의의 비밀번호 새로 고침"), ("Set your own password", "자신만의 비밀번호 설정"), - ("Enable keyboard/mouse", "키보드/마우스 사용함"), - ("Enable clipboard", "클립보드 사용함"), - ("Enable file transfer", "파일 전송 사용함"), - ("Enable TCP tunneling", "TCP 터널링 사용함"), + ("Enable keyboard/mouse", "키보드/마우스 허용"), + ("Enable clipboard", "클립보드 허용"), + ("Enable file transfer", "파일 전송 허용"), + ("Enable TCP tunneling", "TCP 터널링 허용"), ("IP Whitelisting", "IP 화이트리스트"), ("ID/Relay Server", "ID/릴레이 서버"), ("Import server config", "서버 구성 가져오기"), @@ -81,7 +81,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please try 1 minute later", "1분 후에 다시 시도하세요"), ("Login Error", "로그인 오류"), ("Successful", "성공"), - ("Connected, waiting for image...", "연결되었습니다, 이미지를 기다리는 중..."), + ("Connected, waiting for image...", "연결됨, 화면을 기다리는 중..."), ("Name", "이름"), ("Type", "유형"), ("Modified", "수정 날짜"), @@ -142,7 +142,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to connect to relay server", "릴레이 서버 연결에 실패했습니다"), ("Failed to connect via rendezvous server", "랑데부 서버를 통한 연결에 실패했습니다"), ("Failed to connect via relay server", "릴레이 서버를 통한 연결에 실패했습니다"), - ("Failed to make direct connection to remote desktop", "원격 데스크탑에 직접 연결에 실패했습니다"), + ("Failed to make direct connection to remote desktop", "원격 데스크탑 직접 연결에 실패했습니다"), ("Set Password", "비밀번호 설정"), ("OS Password", "OS 비밀번호"), ("install_tip", "UAC로 인해 경우에 따라 RustDesk가 원격 쪽에서 제대로 작동하지 않을 수 있습니다. UAC를 피하려면 아래 버튼을 클릭하여 시스템에 RustDesk를 설치하세요."), @@ -162,7 +162,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Generating ...", "생성 중 ..."), ("Your installation is lower version.", "설치된 버전이 낮습니다."), ("not_close_tcp_tip", "터널을 사용하는 동안에는 이 창을 닫지 마세요"), - ("Listening ...", "청취 중 ..."), + ("Listening ...", "수신 대기 중 ..."), ("Remote Host", "원격 호스트"), ("Remote Port", "원격 포트"), ("Action", "동작"), @@ -177,7 +177,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept", "수락"), ("Dismiss", "거부"), ("Disconnect", "연결 해제"), - ("Enable file copy and paste", "파일 복사 및 붙여넣기 사용함"), + ("Enable file copy and paste", "파일 복사 및 붙여넣기 허용"), ("Connected", "연결됨"), ("Direct and encrypted connection", "직접 및 암호화된 연결"), ("Relayed and encrypted connection", "릴레이 및 암호화된 연결"), @@ -186,9 +186,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enter Remote ID", "원격 ID 입력"), ("Enter your password", "비밀번호 입력"), ("Logging in...", "로그인 중..."), - ("Enable RDP session sharing", "RDP 세션 공유 사용함"), + ("Enable RDP session sharing", "RDP 세션 공유 허용"), ("Auto Login", "자동 로그인"), - ("Enable direct IP access", "직접 IP 액세스 사용함"), + ("Enable direct IP access", "직접 IP 액세스 허용"), ("Rename", "이름 바꾸기"), ("Space", "공백"), ("Create desktop shortcut", "바탕 화면 바로가기 만들기"), @@ -200,13 +200,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Login screen using Wayland is not supported", "Wayland를 사용한 로그인 화면은 지원되지 않습니다"), ("Reboot required", "재부팅이 필요합니다"), ("Unsupported display server", "지원하지 않는 디스플레이 서버"), - ("x11 expected", "x11 예상"), + ("x11 expected", "x11 환경이 필요합니다"), ("Port", "포트"), ("Settings", "설정"), ("Username", "사용자 이름"), ("Invalid port", "유효하지 않은 포트입니다"), ("Closed manually by the peer", "피어가 수동으로 닫았습니다"), - ("Enable remote configuration modification", "원격 구성 수정 사용함"), + ("Enable remote configuration modification", "원격 구성 수정 허용"), ("Run without install", "설치 없이 실행"), ("Connect via relay", "릴레이를 통해 연결"), ("Always connect via relay", "항상 릴레이를 통해 연결"), @@ -214,7 +214,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Login", "로그인"), ("Verify", "확인"), ("Remember me", "기억하기"), - ("Trust this device", "이 장치 신뢰"), + ("Trust this device", "이 장치를 신뢰"), ("Verification code", "인증 코드"), ("verification_tip", "등록한 이메일 주소로 인증 코드가 전송되었으니 인증 코드를 입력하여 로그인을 계속하세요."), ("Logout", "로그아웃"), @@ -291,7 +291,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Help", "도움말"), ("Failed", "실패"), ("Succeeded", "성공"), - ("Someone turns on privacy mode, exit", "누군가가 개인정보 보호 모드를 켭니다, 종료합니다"), + ("Someone turns on privacy mode, exit", "누군가 개인정보 보호 모드를 켰습니다, 연결을 종료합니다"), ("Unsupported", "지원되지 않음"), ("Peer denied", "연결 거부됨"), ("Please install plugins", "플러그인을 설치해주세요"), @@ -311,7 +311,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use permanent password", "영구 비밀번호 사용"), ("Use both passwords", "두 가지 비밀번호 모두 사용"), ("Set permanent password", "영구 비밀번호 설정"), - ("Enable remote restart", "원격 재시작 사용함"), + ("Enable remote restart", "원격 재시작 허용"), ("Restart remote device", "원격 장치 다시 시작"), ("Are you sure you want to restart", "다시 시작하시겠습니까"), ("Restarting remote device", "원격 장치를 다시 시작하는 중"), @@ -344,7 +344,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Follow System", "시스템 설정 따름"), ("Enable hardware codec", "하드웨어 코덱 활성화"), ("Unlock Security Settings", "보안 설정 잠금 해제"), - ("Enable audio", "오디오 사용함"), + ("Enable audio", "오디오 허용"), ("Unlock Network Settings", "네트워크 설정 잠금 해제"), ("Server", "서버"), ("Direct IP Access", "직접 IP 연결"), @@ -364,8 +364,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change", "변경"), ("Start session recording", "세션 녹화 시작"), ("Stop session recording", "세션 녹화 중지"), - ("Enable recording session", "세션 녹화 사용함"), - ("Enable LAN discovery", "LAN 검색 사용함"), + ("Enable recording session", "세션 녹화 허용"), + ("Enable LAN discovery", "LAN 검색 허용"), ("Deny LAN discovery", "LAN 검색 거부"), ("Write a message", "메시지 쓰기"), ("Prompt", "프롬프트"), @@ -428,7 +428,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", "약함"), ("Medium", "보통"), ("Strong", "강력"), - ("Switch Sides", "측면 전환"), + ("Switch Sides", "역할 전환"), ("Please confirm if you want to share your desktop?", "데스크탑을 공유하시겠습니까?"), ("Display", "디스플레이"), ("Default View Style", "기본 보기 스타일"), @@ -488,7 +488,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Plugins", "플러그인"), ("Uninstall", "설치 제거"), ("Update", "업데이트"), - ("Enable", "사용함"), + ("Enable", "허용"), ("Disable", "사용 안 함"), ("Options", "옵션"), ("resolution_original_tip", "원본 해상도"), @@ -558,7 +558,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Virtual display", "가상 디스플레이"), ("Plug out all", "모든 플러그를 뽑으세요"), ("True color (4:4:4)", "트루컬러 (4:4:4)"), - ("Enable blocking user input", "사용자 입력 차단 사용함"), + ("Enable blocking user input", "사용자 입력 차단 허용"), ("id_input_tip", "ID, 직접 IP 또는 포트가 있는 도메인 (:)을 입력할 수 있습니다.\n다른 서버에 있는 장치에 액세스하려면 서버 주소 (@?key=)를 추가하세요. 예를들어 \n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\n공용 서버의 장치에 액세스하려면 \"@public\"을 입력하세요. 공용 서버에서는 키가 필요하지 않습니다.\n\n첫 번째 연결에서 릴레이 연결을 강제로 사용하려면 ID 끝에 \"/r\"을 추가합니다, 예를들면 \"9123456234/r\"."), ("privacy_mode_impl_mag_tip", "모드 1"), ("privacy_mode_impl_virtual_display_tip", "모드 2"), @@ -571,7 +571,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("swap-left-right-mouse", "마우스 왼쪽 버튼과 오른쪽 버튼 교체"), ("2FA code", "이중 인증 코드"), ("More", "더 많은"), - ("enable-2fa-title", "이중 인증 사용함"), + ("enable-2fa-title", "이중 인증 허용"), ("enable-2fa-desc", "지금 인증앱을 설정해 주세요. 휴대폰이나 데스크탑에서 Authy, Microsoft 또는 Google 인증기와 같은 인증기 앱을 사용할 수 있습니다.\n\n앱으로 QR 코드를 스캔하고 앱에 표시된 코드를 입력하면 이중 인증이 가능합니다."), ("wrong-2fa-code", "코드를 확인할 수 없습니다. 코드와 현지 시간 설정이 올바른지 확인합니다"), ("enter-2fa-title", "이중 인증"), @@ -632,7 +632,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", "최소 {}자 이상 필요합니다."), ("Wrong PIN", "잘못된 PIN"), ("Set PIN", "PIN 설정"), - ("Enable trusted devices", "신뢰할 수 있는 장치 사용함"), + ("Enable trusted devices", "신뢰할 수 있는 장치 허용"), ("Manage trusted devices", "신뢰할 수 있는 장치 관리"), ("Platform", "플랫폼"), ("Days remaining", "일 남음"), @@ -678,7 +678,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("screenshot-action-tip", "스크린샷을 계속 진행할 방법을 선택해 주세요."), ("Save as", "다른 이름으로 저장"), ("Copy to clipboard", "클립보드에 복사"), - ("Enable remote printer", "원격 프린터 사용함"), + ("Enable remote printer", "원격 프린터 허용"), ("Downloading {}", "{} 다운로드 중"), ("{} Update", "{} 업데이트"), ("{}-to-update-tip", "{}가 지금 닫히고 새 버전을 설치합니다."), @@ -693,11 +693,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable IPv6 P2P connection", "IPv6 P2P 연결 사용"), ("Enable UDP hole punching", "UDP 홀 펀칭 사용"), ("View camera", "카메라 보기"), - ("Enable camera", "카메라 사용함"), + ("Enable camera", "카메라 허용"), ("No cameras", "카메라 없음"), ("view_camera_unsupported_tip", "원격 장치가 카메라 보기를 지원하지 않습니다."), ("Terminal", "터미널"), - ("Enable terminal", "터미널 사용함"), + ("Enable terminal", "터미널 허용"), ("New tab", "새 탭"), ("Keep terminal sessions on disconnect", "연결이 끊어져도 터미널 세션 유지"), ("Terminal (Run as administrator)", "터미널 (관리자 권한으로 실행)"), From a6724b1c07c1ad6416e3bca0cbd66072eb0670b3 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 20 Jan 2026 22:53:18 +0800 Subject: [PATCH 090/277] fix: build (#14093) Signed-off-by: fufesou --- src/lang/ko.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 1193f735e..812c87e7c 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -736,6 +736,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", "커서 잠금에 실패했습니다. 상대 마우스 모드가 비활성화되었습니다"), ("rel-mouse-exit-{}-tip", "종료하려면 {}을(를) 누르세요."), ("rel-mouse-permission-lost-tip", "키보드 권한이 취소되었습니다. 상대 마우스 모드가 비활성화되었습니다."), - ("Changelog", ""변경 기록), + ("Changelog", "변경 기록"), ].iter().cloned().collect(); } From 21a7cef98ab358f3c9df2bd49ebca076e1a3a24f Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Wed, 21 Jan 2026 16:25:57 +0800 Subject: [PATCH 091/277] keep-awake-during-incoming-sessions (#14082) * keep-awake-during-incoming-sessions * Update flutter/lib/desktop/pages/desktop_setting_page.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update flutter/lib/common.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update flutter/lib/mobile/pages/settings_page.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update common.dart * wakelock Signed-off-by: 21pages * fix build Signed-off-by: 21pages * Update server_model.dart --------- Signed-off-by: 21pages Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: 21pages --- flutter/lib/common.dart | 45 ++++++++++++++----- flutter/lib/consts.dart | 3 ++ .../desktop/pages/desktop_setting_page.dart | 14 ++++++ .../lib/mobile/pages/file_manager_page.dart | 6 +-- flutter/lib/mobile/pages/remote_page.dart | 11 ++--- flutter/lib/mobile/pages/settings_page.dart | 16 ++++++- .../lib/mobile/pages/view_camera_page.dart | 11 ++--- flutter/lib/models/server_model.dart | 27 +++++------ libs/hbb_common | 2 +- libs/portable/src/main.rs | 3 ++ src/lang/ar.rs | 2 + src/lang/be.rs | 2 + src/lang/bg.rs | 2 + src/lang/ca.rs | 2 + src/lang/cn.rs | 2 + src/lang/cs.rs | 2 + src/lang/da.rs | 2 + src/lang/de.rs | 2 + src/lang/el.rs | 2 + src/lang/en.rs | 2 + src/lang/eo.rs | 2 + src/lang/es.rs | 2 + src/lang/et.rs | 2 + src/lang/eu.rs | 2 + src/lang/fa.rs | 2 + src/lang/fi.rs | 2 + src/lang/fr.rs | 2 + src/lang/ge.rs | 2 + src/lang/he.rs | 2 + src/lang/hr.rs | 2 + src/lang/hu.rs | 2 + src/lang/id.rs | 2 + src/lang/it.rs | 2 + src/lang/ja.rs | 2 + src/lang/ko.rs | 2 + src/lang/kz.rs | 2 + src/lang/lt.rs | 2 + src/lang/lv.rs | 2 + src/lang/nb.rs | 2 + src/lang/nl.rs | 2 + src/lang/pl.rs | 2 + src/lang/pt_PT.rs | 2 + src/lang/ptbr.rs | 2 + src/lang/ro.rs | 2 + src/lang/ru.rs | 2 + src/lang/sc.rs | 2 + src/lang/sk.rs | 2 + src/lang/sl.rs | 2 + src/lang/sq.rs | 2 + src/lang/sr.rs | 2 + src/lang/sv.rs | 2 + src/lang/ta.rs | 2 + src/lang/template.rs | 2 + src/lang/th.rs | 2 + src/lang/tr.rs | 4 +- src/lang/tw.rs | 2 + src/lang/uk.rs | 2 + src/lang/vi.rs | 2 + src/server/connection.rs | 25 +++++++++-- src/ui/index.tis | 10 +++++ 60 files changed, 219 insertions(+), 52 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index eca7fa05a..0650b1b5b 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1578,7 +1578,7 @@ bool option2bool(String option, String value) { option == kOptionForceAlwaysRelay) { res = value == "Y"; } else { - assert(false); + // "" is true res = value != "N"; } return res; @@ -1596,9 +1596,6 @@ String bool2option(String option, bool b) { option == kOptionForceAlwaysRelay) { res = b ? 'Y' : defaultOptionNo; } else { - if (option != kOptionEnableUdpPunch && option != kOptionEnableIpv6Punch) { - assert(false); - } res = b ? 'Y' : 'N'; } return res; @@ -2684,20 +2681,44 @@ class SimpleWrapper { /// This manager handles multiple tabs within the same isolate. class WakelockManager { static final Set _enabledKeys = {}; + // Don't use WakelockPlus.enabled, it causes error on Android: + // Unhandled Exception: FormatException: Message corrupted + // + // On Linux, multiple enable() calls create only one inhibit, but each disable() + // only releases if _cookie != null. So we need our own _enabled state to avoid + // calling disable() when not enabled. + // See: https://github.com/fluttercommunity/wakelock_plus/blob/0c74e5bbc6aefac57b6c96bb7ef987705ed559ec/wakelock_plus/lib/src/wakelock_plus_linux_plugin.dart#L48 + static bool _enabled = false; - static void enable(UniqueKey key) { - if (isLinux) return; - _enabledKeys.add(key); - WakelockPlus.enable(); + static void enable(UniqueKey key, {bool isServer = false}) { + // Check if we should keep awake during outgoing sessions + if (!isServer) { + final keepAwake = + mainGetLocalBoolOptionSync(kOptionKeepAwakeDuringOutgoingSessions); + if (!keepAwake) { + return; // Don't enable wakelock if user disabled keep awake + } + } + if (isDesktop) { + _enabledKeys.add(key); + } + if (!_enabled) { + _enabled = true; + WakelockPlus.enable(); + } } static void disable(UniqueKey key) { - if (isLinux) return; - if (_enabledKeys.remove(key)) { - if (_enabledKeys.isEmpty) { - WakelockPlus.disable(); + if (isDesktop) { + _enabledKeys.remove(key); + if (_enabledKeys.isNotEmpty) { + return; } } + if (_enabled) { + WakelockPlus.disable(); + _enabled = false; + } } } diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 78b1f261a..3b9940c9c 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -194,6 +194,9 @@ const String kOptionDisableFloatingWindow = "disable-floating-window"; const String kOptionKeepScreenOn = "keep-screen-on"; +const String kOptionKeepAwakeDuringIncomingSessions = "keep-awake-during-incoming-sessions"; +const String kOptionKeepAwakeDuringOutgoingSessions = "keep-awake-during-outgoing-sessions"; + const String kOptionShowMobileAction = "showMobileActions"; const String kUrlActionClose = "close"; diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index a431efee4..b513bd4d9 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -557,6 +557,17 @@ class _GeneralState extends State<_General> { ], ], ]; + + // Add client-side wakelock option for desktop platforms + if (!bind.isIncomingOnly()) { + children.add(_OptionCheckBox( + context, + 'keep-awake-during-outgoing-sessions-label', + kOptionKeepAwakeDuringOutgoingSessions, + isServer: false, + )); + } + if (!isWeb && bind.mainShowOption(key: kOptionAllowLinuxHeadless)) { children.add(_OptionCheckBox( context, 'Allow linux headless', kOptionAllowLinuxHeadless)); @@ -1219,6 +1230,9 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { ...directIp(context), whitelist(), ...autoDisconnect(context), + _OptionCheckBox(context, 'keep-awake-during-incoming-sessions-label', + kOptionKeepAwakeDuringIncomingSessions, + reverse: false, enabled: enabled), if (bind.mainIsInstalled()) _OptionCheckBox(context, 'allow-only-conn-window-open-tip', 'allow-only-conn-window-open', diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index 745df67b5..1e793bca7 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -5,7 +5,6 @@ import 'package:flutter_breadcrumb/flutter_breadcrumb.dart'; import 'package:flutter_hbb/models/file_model.dart'; import 'package:get/get.dart'; import 'package:toggle_switch/toggle_switch.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; import '../../common.dart'; import '../../common/widgets/dialog.dart'; @@ -72,6 +71,7 @@ class _FileManagerPageState extends State { showLocal ? model.localController : model.remoteController; FileDirectory get currentDir => currentFileController.directory.value; DirectoryOptions get currentOptions => currentFileController.options.value; + final _uniqueKey = UniqueKey(); @override void initState() { @@ -86,7 +86,7 @@ class _FileManagerPageState extends State { .showLoading(translate('Connecting...'), onCancel: closeConnection); }); gFFI.ffiModel.updateEventListener(gFFI.sessionId, widget.id); - WakelockPlus.enable(); + WakelockManager.enable(_uniqueKey); } @override @@ -94,7 +94,7 @@ class _FileManagerPageState extends State { model.close().whenComplete(() { gFFI.close(); gFFI.dialogManager.dismissAll(); - WakelockPlus.disable(); + WakelockManager.disable(_uniqueKey); }); model.jobController.clear(); super.dispose(); diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 22dbebce6..1850f2093 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -14,7 +14,6 @@ import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:flutter_svg/svg.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; import '../../common.dart'; import '../../common/widgets/overlay.dart'; @@ -67,7 +66,7 @@ class _RemotePageState extends State with WidgetsBindingObserver { String _value = ''; Orientation? _currentOrientation; double _viewInsetsBottom = 0; - + final _uniqueKey = UniqueKey(); Timer? _timerDidChangeMetrics; final _blockableOverlayState = BlockableOverlayState(); @@ -105,9 +104,7 @@ class _RemotePageState extends State with WidgetsBindingObserver { gFFI.dialogManager .showLoading(translate('Connecting...'), onCancel: closeConnection); }); - if (!isWeb) { - WakelockPlus.enable(); - } + WakelockManager.enable(_uniqueKey); _physicalFocusNode.requestFocus(); gFFI.inputModel.listenToMouse(true); gFFI.qualityMonitorModel.checkShowQualityMonitor(sessionId); @@ -146,9 +143,7 @@ class _RemotePageState extends State with WidgetsBindingObserver { gFFI.dialogManager.dismissAll(); await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); - if (!isWeb) { - await WakelockPlus.disable(); - } + WakelockManager.disable(_uniqueKey); await keyboardSubscription.cancel(); removeSharedStates(widget.id); // `on_voice_call_closed` should be called when the connection is ended. diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index afe8ae446..c2e2ef57d 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -100,6 +100,7 @@ class _SettingsState extends State with WidgetsBindingObserver { var _enableIpv6Punch = false; var _isUsingPublicServer = false; var _allowAskForNoteAtEndOfConnection = false; + var _preventSleepWhileConnected = true; _SettingsState() { _enableAbr = option2bool( @@ -140,6 +141,8 @@ class _SettingsState extends State with WidgetsBindingObserver { _enableIpv6Punch = mainGetLocalBoolOptionSync(kOptionEnableIpv6Punch); _allowAskForNoteAtEndOfConnection = mainGetLocalBoolOptionSync(kOptionAllowAskForNoteAtEndOfConnection); + _preventSleepWhileConnected = + mainGetLocalBoolOptionSync(kOptionKeepAwakeDuringOutgoingSessions); _showTerminalExtraKeys = mainGetLocalBoolOptionSync(kOptionEnableShowTerminalExtraKeys); } @@ -823,7 +826,18 @@ class _SettingsState extends State with WidgetsBindingObserver { _allowAskForNoteAtEndOfConnection = newValue; }); }, - ) + ), + if (!incomingOnly) + SettingsTile.switchTile( + title: Text(translate('keep-awake-during-outgoing-sessions-label')), + initialValue: _preventSleepWhileConnected, + onToggle: (v) async { + await mainSetLocalBoolOption(kOptionKeepAwakeDuringOutgoingSessions, v); + setState(() { + _preventSleepWhileConnected = v; + }); + }, + ), ]), if (isAndroid) SettingsSection(title: Text(translate('Hardware Codec')), tiles: [ diff --git a/flutter/lib/mobile/pages/view_camera_page.dart b/flutter/lib/mobile/pages/view_camera_page.dart index 018d22980..0898125c4 100644 --- a/flutter/lib/mobile/pages/view_camera_page.dart +++ b/flutter/lib/mobile/pages/view_camera_page.dart @@ -11,7 +11,6 @@ import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:flutter_svg/svg.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; import '../../common.dart'; import '../../common/widgets/overlay.dart'; @@ -62,7 +61,7 @@ class _ViewCameraPageState extends State bool _showGestureHelp = false; Orientation? _currentOrientation; double _viewInsetsBottom = 0; - + final _uniqueKey = UniqueKey(); Timer? _timerDidChangeMetrics; final _blockableOverlayState = BlockableOverlayState(); @@ -100,9 +99,7 @@ class _ViewCameraPageState extends State gFFI.dialogManager .showLoading(translate('Connecting...'), onCancel: closeConnection); }); - if (!isWeb) { - WakelockPlus.enable(); - } + WakelockManager.enable(_uniqueKey); _physicalFocusNode.requestFocus(); gFFI.inputModel.listenToMouse(true); gFFI.qualityMonitorModel.checkShowQualityMonitor(sessionId); @@ -139,9 +136,7 @@ class _ViewCameraPageState extends State gFFI.dialogManager.dismissAll(); await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); - if (!isWeb) { - await WakelockPlus.disable(); - } + WakelockManager.disable(_uniqueKey); removeSharedStates(widget.id); // `on_voice_call_closed` should be called when the connection is ended. // The inner logic of `on_voice_call_closed` will check if the voice call is active. diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index c3e6fab71..8ead158ac 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -8,7 +8,6 @@ import 'package:flutter_hbb/mobile/pages/settings_page.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:get/get.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:window_manager/window_manager.dart'; import '../common.dart'; @@ -51,6 +50,8 @@ class ServerModel with ChangeNotifier { Timer? cmHiddenTimer; + final _wakelockKey = UniqueKey(); + bool get isStart => _isStart; bool get mediaOk => _mediaOk; @@ -466,10 +467,8 @@ class ServerModel with ChangeNotifier { await parent.target?.invokeMethod("stop_service"); await bind.mainStopService(); notifyListeners(); - if (!isLinux) { - // current linux is not supported - WakelockPlus.disable(); - } + // for androidUpdatekeepScreenOn only + WakelockManager.disable(_wakelockKey); } Future setPermanentPassword(String newPW) async { @@ -613,12 +612,12 @@ class ServerModel with ChangeNotifier { void showLoginDialog(Client client) { showClientDialog( client, - client.isFileTransfer - ? "Transfer file" + client.isFileTransfer + ? "Transfer file" : client.isViewCamera ? "View camera" - : client.isTerminal - ? "Terminal" + : client.isTerminal + ? "Terminal" : "Share screen", 'Do you accept?', 'android_new_connection_tip', @@ -797,12 +796,10 @@ class ServerModel with ChangeNotifier { final on = ((keepScreenOn == KeepScreenOn.serviceOn) && _isStart) || (keepScreenOn == KeepScreenOn.duringControlled && _clients.map((e) => !e.disconnected).isNotEmpty); - if (on != await WakelockPlus.enabled) { - if (on) { - WakelockPlus.enable(); - } else { - WakelockPlus.disable(); - } + if (on) { + WakelockManager.enable(_wakelockKey, isServer: true); + } else { + WakelockManager.disable(_wakelockKey); } } } diff --git a/libs/hbb_common b/libs/hbb_common index 073403edb..7d93d5af4 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 073403edbf1fffcb3acfe8cbe7582ee873b23398 +Subproject commit 7d93d5af48db34dbd4a9d317e4a69d04b0bcf703 diff --git a/libs/portable/src/main.rs b/libs/portable/src/main.rs index 1c754cc74..b7ff44ec5 100644 --- a/libs/portable/src/main.rs +++ b/libs/portable/src/main.rs @@ -187,7 +187,10 @@ fn main() { i += 1; } let click_setup = args.is_empty() && arg_exe.to_lowercase().ends_with("install.exe"); + #[cfg(windows)] let quick_support = args.is_empty() && win::is_quick_support_exe(&arg_exe); + #[cfg(not(windows))] + let quick_support = false; let mut ui = false; let reader = BinaryReader::default(); diff --git a/src/lang/ar.rs b/src/lang/ar.rs index cee43eaad..14f74f048 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 6d090d45f..7e0322deb 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index e7e56f22b..573a7824e 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index fc75a83b9..adbbd3d09 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 1f3b02577..24a2bf5cc 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", "按下 {} 退出"), ("rel-mouse-permission-lost-tip", "键盘权限被撤销。相对鼠标模式已被禁用。"), ("Changelog", "更新日志"), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index ccba57553..ff8b9856a 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index c90fa7118..9d0b6960a 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index f7521daff..86545b3df 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", "Drücken Sie {} zum Beenden."), ("rel-mouse-permission-lost-tip", "Die Tastaturberechtigung wurde widerrufen. Der relative Mausmodus wurde deaktiviert."), ("Changelog", "Änderungsprotokoll"), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index edfa93e55..caf0b4566 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 60cb7b123..1399601de 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -267,5 +267,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-lock-failed-tip", "Failed to lock cursor. Relative Mouse Mode has been disabled."), ("rel-mouse-exit-{}-tip", "Press {} to exit."), ("rel-mouse-permission-lost-tip", "Keyboard permission was revoked. Relative Mouse Mode has been disabled."), + ("keep-awake-during-outgoing-sessions-label", "Keep screen awake during outgoing sessions"), + ("keep-awake-during-incoming-sessions-label", "Keep screen awake during incoming sessions"), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index c41845731..5edd85ccf 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index d6958e643..a6e010568 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index f78990e99..910db4df7 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index cb1fdc143..daaedb24c 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 18d331007..47df53bc9 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fi.rs b/src/lang/fi.rs index 00f9692c4..d63d8ce20 100644 --- a/src/lang/fi.rs +++ b/src/lang/fi.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 6a4f4b562..a5deb4596 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", "Appuyez sur {} pour quitter."), ("rel-mouse-permission-lost-tip", "L’autorisation de contrôle du clavier a été révoquée. Le mode souris relative a été désactivé."), ("Changelog", "Journal des modifications"), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ge.rs b/src/lang/ge.rs index e59fca4dd..178906587 100644 --- a/src/lang/ge.rs +++ b/src/lang/ge.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index a92905bd9..3a58c1235 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index e998b0672..b946ab2de 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index d7b82ff7a..609773681 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", "A kilépéshez nyomja meg a(z) {} gombot."), ("rel-mouse-permission-lost-tip", "A billentyűzet-hozzáférés vissza lett vonva. A relatív egérmód le lett tilva."), ("Changelog", "Változáslista"), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 0bd200e4b..d4e6290ac 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 3785f5e0d..5bb4f2349 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", "Premi {} per uscire."), ("rel-mouse-permission-lost-tip", "È stata revocato l'accesso alla tastiera. La modalità mouse relativa è stata disabilitata."), ("Changelog", "Novità programma"), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index fd479c266..989432c87 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 812c87e7c..1c3200629 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", "종료하려면 {}을(를) 누르세요."), ("rel-mouse-permission-lost-tip", "키보드 권한이 취소되었습니다. 상대 마우스 모드가 비활성화되었습니다."), ("Changelog", "변경 기록"), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 9f5cabc78..74a709f46 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 0e0711d4d..fd0c0df77 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 20a1abb94..820b67f1e 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 67bfebdf7..e812b174b 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 2c3400dc8..c5627abfd 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", "Druk op {} om af te sluiten."), ("rel-mouse-permission-lost-tip", "De toetsenbordcontrole is uitgeschakeld. De relatieve muismodus is uitgeschakeld."), ("Changelog", "Wijzigingenlogboek"), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 955d55b2c..9f71948ab 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", "Aby wyłączyć tryb przechwytywania myszy, naciśnij {}"), ("rel-mouse-permission-lost-tip", "Utracono uprawnienia do trybu przechwytywania myszy"), ("Changelog", "Dziennik zmian"), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index d97013c90..0a851273c 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 25624b87f..ed8f2a4ba 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index bd76b34c3..54469bfda 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index ecc768a59..d9a7f15b7 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", "Нажмите {} для выхода."), ("rel-mouse-permission-lost-tip", "Разрешение на использование клавиатуры отменено. Режим относительного перемещения мыши отключён."), ("Changelog", "Журнал изменений"), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index a775bf234..ef1e160b2 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index efbcac7ed..75ef252e9 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index bf0a1e6b4..eb757f613 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 8f1e333a4..adf64a108 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 407725e9b..ae2170c28 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index d82883dc2..917306a30 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs index a4ac03d78..460b0dca9 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index a0a8e31c8..936eef3e1 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 86b3522d3..a36b7f61b 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 00b76b0c3..f81bfdca7 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -735,7 +735,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-not-ready-tip", "Göreli fare modu henüz hazır değil"), ("rel-mouse-lock-failed-tip", "Göreli fare kilitlenemedi"), ("rel-mouse-exit-{}-tip", "Göreli fare modundan çıkmak için {}"), - ("rel-mouse-permission-lost-tip", "Göreli fare izinleri geçerli değil"), + ("rel-mouse-permission-lost-tip", "Göreli fare izinleri geçerli değil"), ("Changelog", "Değişiklik Günlüğü"), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index e93ae0f15..6bde1e7c8 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 7c58f7e91..8c2acdd3e 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", ""), ("rel-mouse-permission-lost-tip", ""), ("Changelog", ""), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vi.rs b/src/lang/vi.rs index 3d03966da..4f9611840 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -737,5 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", "Thoát chế độ chuột tương đối: {}"), ("rel-mouse-permission-lost-tip", "Mất quyền điều khiển chuột tương đối."), ("Changelog", "Nhật ký thay đổi"), + ("keep-awake-during-outgoing-sessions-label", ""), + ("keep-awake-during-incoming-sessions-label", ""), ].iter().cloned().collect(); } diff --git a/src/server/connection.rs b/src/server/connection.rs index d28373459..f90aad115 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -74,6 +74,7 @@ lazy_static::lazy_static! { pub static ref CONTROL_PERMISSIONS_ARRAY: Arc::>> = Default::default(); static ref SWITCH_SIDES_UUID: Arc::>> = Default::default(); static ref WAKELOCK_SENDER: Arc::>> = Arc::new(Mutex::new(start_wakelock_thread())); + static ref WAKELOCK_KEEP_AWAKE_OPTION: Arc::>> = Default::default(); } #[cfg(any(target_os = "windows", target_os = "linux"))] @@ -906,6 +907,7 @@ impl Connection { _ = second_timer.tick() => { #[cfg(windows)] conn.portable_check(); + raii::AuthedConnID::check_wake_lock_on_setting_changed(); if let Some((instant, minute)) = conn.auto_disconnect_timer.as_ref() { if instant.elapsed().as_secs() > minute * 60 { conn.send_close_reason_no_retry("Connection failed due to inactivity").await; @@ -5008,6 +5010,7 @@ impl FileRemoveLogControl { } fn start_wakelock_thread() -> std::sync::mpsc::Sender<(usize, usize)> { + // Check if we should keep awake during incoming sessions use crate::platform::{get_wakelock, WakeLock}; let (tx, rx) = std::sync::mpsc::channel::<(usize, usize)>(); std::thread::spawn(move || { @@ -5016,9 +5019,15 @@ fn start_wakelock_thread() -> std::sync::mpsc::Sender<(usize, usize)> { loop { match rx.recv() { Ok((conn_count, remote_count)) => { - if conn_count == 0 { - wakelock = None; - log::info!("drop wakelock"); + let keep_awake = config::Config::get_bool_option( + keys::OPTION_KEEP_AWAKE_DURING_INCOMING_SESSIONS, + ); + *WAKELOCK_KEEP_AWAKE_OPTION.lock().unwrap() = Some(keep_awake); + if conn_count == 0 || !keep_awake { + if wakelock.is_some() { + wakelock = None; + log::info!("drop wakelock"); + } } else { let mut display = remote_count > 0; if let Some(_w) = wakelock.as_mut() { @@ -5329,6 +5338,16 @@ mod raii { .send((conn_count, remote_count))); } + pub fn check_wake_lock_on_setting_changed() { + let current = config::Config::get_bool_option( + keys::OPTION_KEEP_AWAKE_DURING_INCOMING_SESSIONS, + ); + let cached = *WAKELOCK_KEEP_AWAKE_OPTION.lock().unwrap(); + if cached != Some(current) { + Self::check_wake_lock(); + } + } + #[cfg(windows)] pub fn non_port_forward_conn_count() -> usize { AUTHED_CONNS diff --git a/src/ui/index.tis b/src/ui/index.tis index 8dd4da3d4..09aa0c306 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -268,6 +268,7 @@ class Enhancements: Reactor.Component {
  • {svg_checkmark}{translate("Adaptive bitrate")} (beta)
  • {translate("Recording")}
  • {support_remove_wallpaper ?
  • {svg_checkmark}{translate("Remove wallpaper during incoming sessions")}
  • : ""} +
  • {svg_checkmark}{translate("keep-awake-during-incoming-sessions-label")}
  • ; } @@ -288,6 +289,13 @@ class Enhancements: Reactor.Component { if (is_opt_fixed) { el.state.disabled = true; } + } else if (el.id == "keep-awake-during-incoming-sessions") { + var enabled = handler.get_option(el.id) != "N"; + el.attributes.toggleClass("selected", enabled); + var is_opt_fixed = handler.is_option_fixed(el.id); + if (is_opt_fixed) { + el.state.disabled = true; + } } } @@ -304,6 +312,8 @@ class Enhancements: Reactor.Component { } } else if (v.indexOf("allow-") == 0) { handler.set_option(v, handler.get_option(v) == 'Y' ? default_option_no : 'Y'); + } else if (v == 'keep-awake-during-incoming-sessions') { + handler.set_option(v, handler.get_option(v) != 'N' ? 'N' : default_option_yes); } else if (v == 'screen-recording') { var show_root_dir = is_win && handler.is_installed(); var user_dir = handler.video_save_directory(false); From be4bbd018dd2b9907e82b798df050683536a6c6a Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 21 Jan 2026 20:43:15 +0800 Subject: [PATCH 092/277] fix(install): linux xdo (#14096) Signed-off-by: fufesou --- Cargo.lock | 10 +- Cargo.toml | 7 +- libs/enigo/Cargo.toml | 3 + libs/enigo/src/linux/xdo.rs | 163 ++++++----- libs/hbb_common | 2 +- libs/libxdo-sys-stub/Cargo.toml | 9 + libs/libxdo-sys-stub/src/lib.rs | 505 ++++++++++++++++++++++++++++++++ res/rpm-flutter-suse.spec | 4 +- res/rpm-flutter.spec | 4 +- res/rpm-suse.spec | 4 +- res/rpm.spec | 4 +- src/platform/linux.rs | 92 +++--- 12 files changed, 666 insertions(+), 141 deletions(-) create mode 100644 libs/libxdo-sys-stub/Cargo.toml create mode 100644 libs/libxdo-sys-stub/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 2c8cf996d..5aec38900 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2517,6 +2517,7 @@ version = "0.0.14" dependencies = [ "core-graphics 0.22.3", "hbb_common", + "libxdo-sys", "log", "objc", "pkg-config", @@ -3720,6 +3721,7 @@ dependencies = [ "httparse", "lazy_static", "libc", + "libloading 0.8.4", "log", "mac_address", "machine-uid", @@ -3755,6 +3757,7 @@ dependencies = [ "webrtc", "whoami", "winapi 0.3.9", + "x11 2.21.0", "zstd 0.13.1", ] @@ -4546,11 +4549,8 @@ dependencies = [ [[package]] name = "libxdo-sys" version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db23b9e7e2b7831bbd8aac0bbeeeb7b68cbebc162b227e7052e8e55829a09212" dependencies = [ - "libc", - "x11 2.21.0", + "hbb_common", ] [[package]] @@ -7181,9 +7181,9 @@ dependencies = [ "kcp-sys", "keepawake", "lazy_static", - "libloading 0.8.4", "libpulse-binding", "libpulse-simple-binding", + "libxdo-sys", "mac_address", "magnum-opus", "nix 0.29.0", diff --git a/Cargo.toml b/Cargo.toml index 890da5647..ac1050bf7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,7 +76,6 @@ crossbeam-queue = "0.3" hex = "0.4" chrono = "0.4" cidr-utils = "0.5" -libloading = "0.8" fon = "0.6" zip = "0.6" shutdown_hooks = "0.1" @@ -177,6 +176,7 @@ bytemuck = "1.23" ttf-parser = "0.25" [target.'cfg(target_os = "linux")'.dependencies] +libxdo-sys = "0.11" psimple = { package = "libpulse-simple-binding", version = "2.27" } pulse = { package = "libpulse-binding", version = "2.27" } rust-pulsectl = { git = "https://github.com/rustdesk-org/pulsectl" } @@ -207,6 +207,11 @@ android-wakelock = { git = "https://github.com/rustdesk-org/android-wakelock" } members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable", "libs/remote_printer"] exclude = ["vdi/host", "examples/custom_plugin"] +# Patch libxdo-sys to use a stub implementation that doesn't require libxdo +# This allows building and running on systems without libxdo installed (e.g., Wayland-only) +[patch.crates-io] +libxdo-sys = { path = "libs/libxdo-sys-stub" } + [package.metadata.winres] LegalCopyright = "Copyright © 2025 Purslane Ltd. All rights reserved." ProductName = "RustDesk" diff --git a/libs/enigo/Cargo.toml b/libs/enigo/Cargo.toml index a5b6d5622..6468eeedd 100644 --- a/libs/enigo/Cargo.toml +++ b/libs/enigo/Cargo.toml @@ -37,5 +37,8 @@ core-graphics = "0.22" objc = "0.2" unicode-segmentation = "1.10" +[target.'cfg(target_os = "linux")'.dependencies] +libxdo-sys = "0.11" + [build-dependencies] pkg-config = "0.3" diff --git a/libs/enigo/src/linux/xdo.rs b/libs/enigo/src/linux/xdo.rs index f0f7d49af..26d090855 100644 --- a/libs/enigo/src/linux/xdo.rs +++ b/libs/enigo/src/linux/xdo.rs @@ -1,50 +1,22 @@ +//! XDO-based input emulation for Linux. +//! +//! This module uses libxdo-sys (patched to use dynamic loading stub) for input emulation. +//! The stub handles dynamic loading of libxdo, so we just call the functions directly. +//! +//! If libxdo is not available at runtime, all operations become no-ops. + use crate::{Key, KeyboardControllable, MouseButton, MouseControllable}; -use hbb_common::libc::{c_char, c_int, c_void, useconds_t}; -use std::{borrow::Cow, ffi::CString, ptr}; +use hbb_common::libc::c_int; +use libxdo_sys::{self, xdo_t, CURRENTWINDOW}; +use std::{borrow::Cow, ffi::CString}; -const CURRENT_WINDOW: c_int = 0; +/// Default delay per keypress in microseconds. +/// This value is passed to libxdo functions and must fit in `useconds_t` (u32). const DEFAULT_DELAY: u64 = 12000; -type Window = c_int; -type Xdo = *const c_void; -#[link(name = "xdo")] -extern "C" { - fn xdo_free(xdo: Xdo); - fn xdo_new(display: *const c_char) -> Xdo; - - fn xdo_click_window(xdo: Xdo, window: Window, button: c_int) -> c_int; - fn xdo_mouse_down(xdo: Xdo, window: Window, button: c_int) -> c_int; - fn xdo_mouse_up(xdo: Xdo, window: Window, button: c_int) -> c_int; - fn xdo_move_mouse(xdo: Xdo, x: c_int, y: c_int, screen: c_int) -> c_int; - fn xdo_move_mouse_relative(xdo: Xdo, x: c_int, y: c_int) -> c_int; - - fn xdo_enter_text_window( - xdo: Xdo, - window: Window, - string: *const c_char, - delay: useconds_t, - ) -> c_int; - fn xdo_send_keysequence_window( - xdo: Xdo, - window: Window, - string: *const c_char, - delay: useconds_t, - ) -> c_int; - fn xdo_send_keysequence_window_down( - xdo: Xdo, - window: Window, - string: *const c_char, - delay: useconds_t, - ) -> c_int; - fn xdo_send_keysequence_window_up( - xdo: Xdo, - window: Window, - string: *const c_char, - delay: useconds_t, - ) -> c_int; - fn xdo_get_input_state(xdo: Xdo) -> u32; -} +/// Maximum allowed delay value (u32::MAX as u64). +const MAX_DELAY: u64 = u32::MAX as u64; fn mousebutton(button: MouseButton) -> c_int { match button { @@ -62,7 +34,7 @@ fn mousebutton(button: MouseButton) -> c_int { /// The main struct for handling the event emitting pub(super) struct EnigoXdo { - xdo: Xdo, + xdo: *mut xdo_t, delay: u64, } // This is safe, we have a unique pointer. @@ -70,37 +42,61 @@ pub(super) struct EnigoXdo { unsafe impl Send for EnigoXdo {} impl Default for EnigoXdo { - /// Create a new EnigoXdo instance + /// Create a new EnigoXdo instance. + /// + /// If libxdo is not available, the xdo pointer will be null and all + /// input operations will be no-ops. fn default() -> Self { + let xdo = unsafe { libxdo_sys::xdo_new(std::ptr::null()) }; + if xdo.is_null() { + log::warn!("Failed to create xdo context, xdo functions will be disabled"); + } else { + log::info!("xdo context created successfully"); + } Self { - xdo: unsafe { xdo_new(ptr::null()) }, + xdo, delay: DEFAULT_DELAY, } } } + impl EnigoXdo { - /// Get the delay per keypress. - /// Default value is 12000. - /// This is Linux-specific. + /// Get the delay per keypress in microseconds. + /// + /// Default value is 12000 (12ms). This is Linux-specific. pub fn delay(&self) -> u64 { self.delay } - /// Set the delay per keypress. - /// This is Linux-specific. + + /// Set the delay per keypress in microseconds. + /// + /// This is Linux-specific. The value is clamped to `u32::MAX` (approximately + /// 4295 seconds) because libxdo uses `useconds_t` which is typically `u32`. + /// + /// # Arguments + /// * `delay` - Delay in microseconds. Values exceeding `u32::MAX` will be clamped. pub fn set_delay(&mut self, delay: u64) { - self.delay = delay; + self.delay = delay.min(MAX_DELAY); + if delay > MAX_DELAY { + log::warn!( + "delay value {} exceeds maximum {}, clamped", + delay, + MAX_DELAY + ); + } } } + impl Drop for EnigoXdo { fn drop(&mut self) { - if self.xdo.is_null() { - return; - } - unsafe { - xdo_free(self.xdo); + if !self.xdo.is_null() { + unsafe { + libxdo_sys::xdo_free(self.xdo); + } } } } + impl MouseControllable for EnigoXdo { fn as_any(&self) -> &dyn std::any::Any { self @@ -115,42 +111,47 @@ impl MouseControllable for EnigoXdo { return; } unsafe { - xdo_move_mouse(self.xdo, x as c_int, y as c_int, 0); + libxdo_sys::xdo_move_mouse(self.xdo as *const _, x, y, 0); } } + fn mouse_move_relative(&mut self, x: i32, y: i32) { if self.xdo.is_null() { return; } unsafe { - xdo_move_mouse_relative(self.xdo, x as c_int, y as c_int); + libxdo_sys::xdo_move_mouse_relative(self.xdo as *const _, x, y); } } + fn mouse_down(&mut self, button: MouseButton) -> crate::ResultType { if self.xdo.is_null() { return Ok(()); } unsafe { - xdo_mouse_down(self.xdo, CURRENT_WINDOW, mousebutton(button)); + libxdo_sys::xdo_mouse_down(self.xdo as *const _, CURRENTWINDOW, mousebutton(button)); } Ok(()) } + fn mouse_up(&mut self, button: MouseButton) { if self.xdo.is_null() { return; } unsafe { - xdo_mouse_up(self.xdo, CURRENT_WINDOW, mousebutton(button)); + libxdo_sys::xdo_mouse_up(self.xdo as *const _, CURRENTWINDOW, mousebutton(button)); } } + fn mouse_click(&mut self, button: MouseButton) { if self.xdo.is_null() { return; } unsafe { - xdo_click_window(self.xdo, CURRENT_WINDOW, mousebutton(button)); + libxdo_sys::xdo_click_window(self.xdo as *const _, CURRENTWINDOW, mousebutton(button)); } } + fn mouse_scroll_x(&mut self, length: i32) { let button; let mut length = length; @@ -169,6 +170,7 @@ impl MouseControllable for EnigoXdo { self.mouse_click(button); } } + fn mouse_scroll_y(&mut self, length: i32) { let button; let mut length = length; @@ -188,6 +190,7 @@ impl MouseControllable for EnigoXdo { } } } + fn keysequence<'a>(key: Key) -> Cow<'a, str> { if let Key::Layout(c) = key { return Cow::Owned(format!("U{:X}", c as u32)); @@ -284,6 +287,7 @@ fn keysequence<'a>(key: Key) -> Cow<'a, str> { _ => "", }) } + impl KeyboardControllable for EnigoXdo { fn as_any(&self) -> &dyn std::any::Any { self @@ -314,7 +318,7 @@ impl KeyboardControllable for EnigoXdo { let mod_alt = 1 << 3; let mod_numlock = 1 << 4; let mod_meta = 1 << 6; - let mask = unsafe { xdo_get_input_state(self.xdo) }; + let mask = unsafe { libxdo_sys::xdo_get_input_state(self.xdo as *const _) }; match key { Key::Shift => mask & mod_shift != 0, Key::CapsLock => mask & mod_lock != 0, @@ -332,56 +336,59 @@ impl KeyboardControllable for EnigoXdo { } if let Ok(string) = CString::new(sequence) { unsafe { - xdo_enter_text_window( - self.xdo, - CURRENT_WINDOW, + libxdo_sys::xdo_enter_text_window( + self.xdo as *const _, + CURRENTWINDOW, string.as_ptr(), - self.delay as useconds_t, + self.delay as libxdo_sys::useconds_t, ); } } } + fn key_down(&mut self, key: Key) -> crate::ResultType { if self.xdo.is_null() { return Ok(()); } let string = CString::new(&*keysequence(key))?; unsafe { - xdo_send_keysequence_window_down( - self.xdo, - CURRENT_WINDOW, + libxdo_sys::xdo_send_keysequence_window_down( + self.xdo as *const _, + CURRENTWINDOW, string.as_ptr(), - self.delay as useconds_t, + self.delay as libxdo_sys::useconds_t, ); } Ok(()) } + fn key_up(&mut self, key: Key) { if self.xdo.is_null() { return; } if let Ok(string) = CString::new(&*keysequence(key)) { unsafe { - xdo_send_keysequence_window_up( - self.xdo, - CURRENT_WINDOW, + libxdo_sys::xdo_send_keysequence_window_up( + self.xdo as *const _, + CURRENTWINDOW, string.as_ptr(), - self.delay as useconds_t, + self.delay as libxdo_sys::useconds_t, ); } } } + fn key_click(&mut self, key: Key) { if self.xdo.is_null() { return; } if let Ok(string) = CString::new(&*keysequence(key)) { unsafe { - xdo_send_keysequence_window( - self.xdo, - CURRENT_WINDOW, + libxdo_sys::xdo_send_keysequence_window( + self.xdo as *const _, + CURRENTWINDOW, string.as_ptr(), - self.delay as useconds_t, + self.delay as libxdo_sys::useconds_t, ); } } diff --git a/libs/hbb_common b/libs/hbb_common index 7d93d5af4..900077a2c 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 7d93d5af48db34dbd4a9d317e4a69d04b0bcf703 +Subproject commit 900077a2c2651336317f8094ea44074c48acd2a4 diff --git a/libs/libxdo-sys-stub/Cargo.toml b/libs/libxdo-sys-stub/Cargo.toml new file mode 100644 index 000000000..0b52cfb63 --- /dev/null +++ b/libs/libxdo-sys-stub/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "libxdo-sys" +version = "0.11.0" +edition = "2021" +publish = false +description = "Dynamic loading wrapper for libxdo-sys that doesn't require libxdo at compile/link time" + +[dependencies] +hbb_common = { path = "../hbb_common" } diff --git a/libs/libxdo-sys-stub/src/lib.rs b/libs/libxdo-sys-stub/src/lib.rs new file mode 100644 index 000000000..53d0e099c --- /dev/null +++ b/libs/libxdo-sys-stub/src/lib.rs @@ -0,0 +1,505 @@ +//! Dynamic loading wrapper for libxdo. +//! +//! Provides the same API as libxdo-sys but loads libxdo at runtime, +//! allowing the program to run on systems without libxdo installed +//! (e.g., Wayland-only environments). + +use hbb_common::{ + libc::{c_char, c_int, c_uint}, + libloading::{Library, Symbol}, + log, +}; +use std::sync::OnceLock; + +pub use hbb_common::x11::xlib::{Display, Screen, Window}; + +#[repr(C)] +pub struct xdo_t { + _private: [u8; 0], +} + +#[repr(C)] +pub struct charcodemap_t { + _private: [u8; 0], +} + +#[repr(C)] +pub struct xdo_search_t { + _private: [u8; 0], +} + +pub type useconds_t = c_uint; + +pub const CURRENTWINDOW: Window = 0; + +type FnXdoNew = unsafe extern "C" fn(*const c_char) -> *mut xdo_t; +type FnXdoNewWithOpenedDisplay = + unsafe extern "C" fn(*mut Display, *const c_char, c_int) -> *mut xdo_t; +type FnXdoFree = unsafe extern "C" fn(*mut xdo_t); +type FnXdoSendKeysequenceWindow = + unsafe extern "C" fn(*const xdo_t, Window, *const c_char, useconds_t) -> c_int; +type FnXdoSendKeysequenceWindowDown = + unsafe extern "C" fn(*const xdo_t, Window, *const c_char, useconds_t) -> c_int; +type FnXdoSendKeysequenceWindowUp = + unsafe extern "C" fn(*const xdo_t, Window, *const c_char, useconds_t) -> c_int; +type FnXdoEnterTextWindow = + unsafe extern "C" fn(*const xdo_t, Window, *const c_char, useconds_t) -> c_int; +type FnXdoClickWindow = unsafe extern "C" fn(*const xdo_t, Window, c_int) -> c_int; +type FnXdoMouseDown = unsafe extern "C" fn(*const xdo_t, Window, c_int) -> c_int; +type FnXdoMouseUp = unsafe extern "C" fn(*const xdo_t, Window, c_int) -> c_int; +type FnXdoMoveMouse = unsafe extern "C" fn(*const xdo_t, c_int, c_int, c_int) -> c_int; +type FnXdoMoveMouseRelative = unsafe extern "C" fn(*const xdo_t, c_int, c_int) -> c_int; +type FnXdoMoveMouseRelativeToWindow = + unsafe extern "C" fn(*const xdo_t, Window, c_int, c_int) -> c_int; +type FnXdoGetMouseLocation = + unsafe extern "C" fn(*const xdo_t, *mut c_int, *mut c_int, *mut c_int) -> c_int; +type FnXdoGetMouseLocation2 = + unsafe extern "C" fn(*const xdo_t, *mut c_int, *mut c_int, *mut c_int, *mut Window) -> c_int; +type FnXdoGetActiveWindow = unsafe extern "C" fn(*const xdo_t, *mut Window) -> c_int; +type FnXdoGetFocusedWindow = unsafe extern "C" fn(*const xdo_t, *mut Window) -> c_int; +type FnXdoGetFocusedWindowSane = unsafe extern "C" fn(*const xdo_t, *mut Window) -> c_int; +type FnXdoGetWindowLocation = + unsafe extern "C" fn(*const xdo_t, Window, *mut c_int, *mut c_int, *mut *mut Screen) -> c_int; +type FnXdoGetWindowSize = + unsafe extern "C" fn(*const xdo_t, Window, *mut c_uint, *mut c_uint) -> c_int; +type FnXdoGetInputState = unsafe extern "C" fn(*const xdo_t) -> c_uint; +type FnXdoActivateWindow = unsafe extern "C" fn(*const xdo_t, Window) -> c_int; +type FnXdoWaitForMouseMoveFrom = unsafe extern "C" fn(*const xdo_t, c_int, c_int) -> c_int; +type FnXdoWaitForMouseMoveTo = unsafe extern "C" fn(*const xdo_t, c_int, c_int) -> c_int; +type FnXdoSetWindowClass = + unsafe extern "C" fn(*const xdo_t, Window, *const c_char, *const c_char) -> c_int; +type FnXdoSearchWindows = + unsafe extern "C" fn(*const xdo_t, *const xdo_search_t, *mut *mut Window, *mut c_uint) -> c_int; + +struct XdoLib { + _lib: Library, + xdo_new: FnXdoNew, + xdo_new_with_opened_display: Option, + xdo_free: FnXdoFree, + xdo_send_keysequence_window: FnXdoSendKeysequenceWindow, + xdo_send_keysequence_window_down: Option, + xdo_send_keysequence_window_up: Option, + xdo_enter_text_window: Option, + xdo_click_window: Option, + xdo_mouse_down: Option, + xdo_mouse_up: Option, + xdo_move_mouse: Option, + xdo_move_mouse_relative: Option, + xdo_move_mouse_relative_to_window: Option, + xdo_get_mouse_location: Option, + xdo_get_mouse_location2: Option, + xdo_get_active_window: Option, + xdo_get_focused_window: Option, + xdo_get_focused_window_sane: Option, + xdo_get_window_location: Option, + xdo_get_window_size: Option, + xdo_get_input_state: Option, + xdo_activate_window: Option, + xdo_wait_for_mouse_move_from: Option, + xdo_wait_for_mouse_move_to: Option, + xdo_set_window_class: Option, + xdo_search_windows: Option, +} + +impl XdoLib { + fn load() -> Option { + // https://github.com/rustdesk/rustdesk/issues/13711 + const LIB_NAMES: [&str; 3] = ["libxdo.so.4", "libxdo.so.3", "libxdo.so"]; + + unsafe { + let (lib, lib_name) = LIB_NAMES + .iter() + .find_map(|name| Library::new(name).ok().map(|lib| (lib, *name)))?; + + log::info!("libxdo-sys Loaded {}", lib_name); + + let xdo_new: FnXdoNew = *lib.get(b"xdo_new").ok()?; + let xdo_free: FnXdoFree = *lib.get(b"xdo_free").ok()?; + let xdo_send_keysequence_window: FnXdoSendKeysequenceWindow = + *lib.get(b"xdo_send_keysequence_window").ok()?; + + let xdo_new_with_opened_display = lib + .get(b"xdo_new_with_opened_display") + .ok() + .map(|s: Symbol| *s); + let xdo_send_keysequence_window_down = lib + .get(b"xdo_send_keysequence_window_down") + .ok() + .map(|s: Symbol| *s); + let xdo_send_keysequence_window_up = lib + .get(b"xdo_send_keysequence_window_up") + .ok() + .map(|s: Symbol| *s); + let xdo_enter_text_window = lib + .get(b"xdo_enter_text_window") + .ok() + .map(|s: Symbol| *s); + let xdo_click_window = lib + .get(b"xdo_click_window") + .ok() + .map(|s: Symbol| *s); + let xdo_mouse_down = lib + .get(b"xdo_mouse_down") + .ok() + .map(|s: Symbol| *s); + let xdo_mouse_up = lib + .get(b"xdo_mouse_up") + .ok() + .map(|s: Symbol| *s); + let xdo_move_mouse = lib + .get(b"xdo_move_mouse") + .ok() + .map(|s: Symbol| *s); + let xdo_move_mouse_relative = lib + .get(b"xdo_move_mouse_relative") + .ok() + .map(|s: Symbol| *s); + let xdo_move_mouse_relative_to_window = lib + .get(b"xdo_move_mouse_relative_to_window") + .ok() + .map(|s: Symbol| *s); + let xdo_get_mouse_location = lib + .get(b"xdo_get_mouse_location") + .ok() + .map(|s: Symbol| *s); + let xdo_get_mouse_location2 = lib + .get(b"xdo_get_mouse_location2") + .ok() + .map(|s: Symbol| *s); + let xdo_get_active_window = lib + .get(b"xdo_get_active_window") + .ok() + .map(|s: Symbol| *s); + let xdo_get_focused_window = lib + .get(b"xdo_get_focused_window") + .ok() + .map(|s: Symbol| *s); + let xdo_get_focused_window_sane = lib + .get(b"xdo_get_focused_window_sane") + .ok() + .map(|s: Symbol| *s); + let xdo_get_window_location = lib + .get(b"xdo_get_window_location") + .ok() + .map(|s: Symbol| *s); + let xdo_get_window_size = lib + .get(b"xdo_get_window_size") + .ok() + .map(|s: Symbol| *s); + let xdo_get_input_state = lib + .get(b"xdo_get_input_state") + .ok() + .map(|s: Symbol| *s); + let xdo_activate_window = lib + .get(b"xdo_activate_window") + .ok() + .map(|s: Symbol| *s); + let xdo_wait_for_mouse_move_from = lib + .get(b"xdo_wait_for_mouse_move_from") + .ok() + .map(|s: Symbol| *s); + let xdo_wait_for_mouse_move_to = lib + .get(b"xdo_wait_for_mouse_move_to") + .ok() + .map(|s: Symbol| *s); + let xdo_set_window_class = lib + .get(b"xdo_set_window_class") + .ok() + .map(|s: Symbol| *s); + let xdo_search_windows = lib + .get(b"xdo_search_windows") + .ok() + .map(|s: Symbol| *s); + + Some(Self { + _lib: lib, + xdo_new, + xdo_new_with_opened_display, + xdo_free, + xdo_send_keysequence_window, + xdo_send_keysequence_window_down, + xdo_send_keysequence_window_up, + xdo_enter_text_window, + xdo_click_window, + xdo_mouse_down, + xdo_mouse_up, + xdo_move_mouse, + xdo_move_mouse_relative, + xdo_move_mouse_relative_to_window, + xdo_get_mouse_location, + xdo_get_mouse_location2, + xdo_get_active_window, + xdo_get_focused_window, + xdo_get_focused_window_sane, + xdo_get_window_location, + xdo_get_window_size, + xdo_get_input_state, + xdo_activate_window, + xdo_wait_for_mouse_move_from, + xdo_wait_for_mouse_move_to, + xdo_set_window_class, + xdo_search_windows, + }) + } + } +} + +static XDO_LIB: OnceLock> = OnceLock::new(); + +fn get_lib() -> Option<&'static XdoLib> { + XDO_LIB + .get_or_init(|| { + let lib = XdoLib::load(); + if lib.is_none() { + log::info!("libxdo-sys libxdo not found, xdo functions will be disabled"); + } + lib + }) + .as_ref() +} + +pub unsafe extern "C" fn xdo_new(display: *const c_char) -> *mut xdo_t { + get_lib().map_or(std::ptr::null_mut(), |lib| (lib.xdo_new)(display)) +} + +pub unsafe extern "C" fn xdo_new_with_opened_display( + xdpy: *mut Display, + display: *const c_char, + close_display_when_freed: c_int, +) -> *mut xdo_t { + get_lib() + .and_then(|lib| lib.xdo_new_with_opened_display) + .map_or(std::ptr::null_mut(), |f| { + f(xdpy, display, close_display_when_freed) + }) +} + +pub unsafe extern "C" fn xdo_free(xdo: *mut xdo_t) { + if xdo.is_null() { + return; + } + if let Some(lib) = get_lib() { + (lib.xdo_free)(xdo); + } +} + +pub unsafe extern "C" fn xdo_send_keysequence_window( + xdo: *const xdo_t, + window: Window, + keysequence: *const c_char, + delay: useconds_t, +) -> c_int { + get_lib().map_or(1, |lib| { + (lib.xdo_send_keysequence_window)(xdo, window, keysequence, delay) + }) +} + +pub unsafe extern "C" fn xdo_send_keysequence_window_down( + xdo: *const xdo_t, + window: Window, + keysequence: *const c_char, + delay: useconds_t, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_send_keysequence_window_down) + .map_or(1, |f| f(xdo, window, keysequence, delay)) +} + +pub unsafe extern "C" fn xdo_send_keysequence_window_up( + xdo: *const xdo_t, + window: Window, + keysequence: *const c_char, + delay: useconds_t, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_send_keysequence_window_up) + .map_or(1, |f| f(xdo, window, keysequence, delay)) +} + +pub unsafe extern "C" fn xdo_enter_text_window( + xdo: *const xdo_t, + window: Window, + string: *const c_char, + delay: useconds_t, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_enter_text_window) + .map_or(1, |f| f(xdo, window, string, delay)) +} + +pub unsafe extern "C" fn xdo_click_window( + xdo: *const xdo_t, + window: Window, + button: c_int, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_click_window) + .map_or(1, |f| f(xdo, window, button)) +} + +pub unsafe extern "C" fn xdo_mouse_down(xdo: *const xdo_t, window: Window, button: c_int) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_mouse_down) + .map_or(1, |f| f(xdo, window, button)) +} + +pub unsafe extern "C" fn xdo_mouse_up(xdo: *const xdo_t, window: Window, button: c_int) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_mouse_up) + .map_or(1, |f| f(xdo, window, button)) +} + +pub unsafe extern "C" fn xdo_move_mouse( + xdo: *const xdo_t, + x: c_int, + y: c_int, + screen: c_int, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_move_mouse) + .map_or(1, |f| f(xdo, x, y, screen)) +} + +pub unsafe extern "C" fn xdo_move_mouse_relative(xdo: *const xdo_t, x: c_int, y: c_int) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_move_mouse_relative) + .map_or(1, |f| f(xdo, x, y)) +} + +pub unsafe extern "C" fn xdo_move_mouse_relative_to_window( + xdo: *const xdo_t, + window: Window, + x: c_int, + y: c_int, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_move_mouse_relative_to_window) + .map_or(1, |f| f(xdo, window, x, y)) +} + +pub unsafe extern "C" fn xdo_get_mouse_location( + xdo: *const xdo_t, + x: *mut c_int, + y: *mut c_int, + screen_num: *mut c_int, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_get_mouse_location) + .map_or(1, |f| f(xdo, x, y, screen_num)) +} + +pub unsafe extern "C" fn xdo_get_mouse_location2( + xdo: *const xdo_t, + x: *mut c_int, + y: *mut c_int, + screen_num: *mut c_int, + window: *mut Window, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_get_mouse_location2) + .map_or(1, |f| f(xdo, x, y, screen_num, window)) +} + +pub unsafe extern "C" fn xdo_get_active_window( + xdo: *const xdo_t, + window_ret: *mut Window, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_get_active_window) + .map_or(1, |f| f(xdo, window_ret)) +} + +pub unsafe extern "C" fn xdo_get_focused_window( + xdo: *const xdo_t, + window_ret: *mut Window, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_get_focused_window) + .map_or(1, |f| f(xdo, window_ret)) +} + +pub unsafe extern "C" fn xdo_get_focused_window_sane( + xdo: *const xdo_t, + window_ret: *mut Window, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_get_focused_window_sane) + .map_or(1, |f| f(xdo, window_ret)) +} + +pub unsafe extern "C" fn xdo_get_window_location( + xdo: *const xdo_t, + window: Window, + x: *mut c_int, + y: *mut c_int, + screen_ret: *mut *mut Screen, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_get_window_location) + .map_or(1, |f| f(xdo, window, x, y, screen_ret)) +} + +pub unsafe extern "C" fn xdo_get_window_size( + xdo: *const xdo_t, + window: Window, + width: *mut c_uint, + height: *mut c_uint, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_get_window_size) + .map_or(1, |f| f(xdo, window, width, height)) +} + +pub unsafe extern "C" fn xdo_get_input_state(xdo: *const xdo_t) -> c_uint { + get_lib() + .and_then(|lib| lib.xdo_get_input_state) + .map_or(0, |f| f(xdo)) +} + +pub unsafe extern "C" fn xdo_activate_window(xdo: *const xdo_t, wid: Window) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_activate_window) + .map_or(1, |f| f(xdo, wid)) +} + +pub unsafe extern "C" fn xdo_wait_for_mouse_move_from( + xdo: *const xdo_t, + origin_x: c_int, + origin_y: c_int, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_wait_for_mouse_move_from) + .map_or(1, |f| f(xdo, origin_x, origin_y)) +} + +pub unsafe extern "C" fn xdo_wait_for_mouse_move_to( + xdo: *const xdo_t, + dest_x: c_int, + dest_y: c_int, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_wait_for_mouse_move_to) + .map_or(1, |f| f(xdo, dest_x, dest_y)) +} + +pub unsafe extern "C" fn xdo_set_window_class( + xdo: *const xdo_t, + wid: Window, + name: *const c_char, + class: *const c_char, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_set_window_class) + .map_or(1, |f| f(xdo, wid, name, class)) +} + +pub unsafe extern "C" fn xdo_search_windows( + xdo: *const xdo_t, + search: *const xdo_search_t, + windowlist_ret: *mut *mut Window, + nwindows_ret: *mut c_uint, +) -> c_int { + get_lib() + .and_then(|lib| lib.xdo_search_windows) + .map_or(1, |f| f(xdo, search, windowlist_ret, nwindows_ret)) +} diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec index d11e0b69a..2049b5f4f 100644 --- a/res/rpm-flutter-suse.spec +++ b/res/rpm-flutter-suse.spec @@ -5,8 +5,8 @@ Summary: RPM package License: GPL-3.0 URL: https://rustdesk.com Vendor: rustdesk -Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire -Recommends: libayatana-appindicator3-1 +Requires: gtk3 libxcb1 libXfixes3 alsa-utils libXtst6 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire +Recommends: libayatana-appindicator3-1 xdotool Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libfile_selector_linux_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit) # https://docs.fedoraproject.org/en-US/packaging-guidelines/Scriptlets/ diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec index 3b6ad5f5d..f8bc7a1a1 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -5,8 +5,8 @@ Summary: RPM package License: GPL-3.0 URL: https://rustdesk.com Vendor: rustdesk -Requires: gtk3 libxcb libxdo libXfixes alsa-lib libva pam gstreamer1-plugins-base -Recommends: libayatana-appindicator-gtk3 +Requires: gtk3 libxcb libXfixes alsa-lib libva pam gstreamer1-plugins-base +Recommends: libayatana-appindicator-gtk3 libxdo Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libfile_selector_linux_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit) # https://docs.fedoraproject.org/en-US/packaging-guidelines/Scriptlets/ diff --git a/res/rpm-suse.spec b/res/rpm-suse.spec index 79b26d6f0..14364eb77 100644 --- a/res/rpm-suse.spec +++ b/res/rpm-suse.spec @@ -3,8 +3,8 @@ Version: 1.1.9 Release: 0 Summary: RPM package License: GPL-3.0 -Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire -Recommends: libayatana-appindicator3-1 +Requires: gtk3 libxcb1 libXfixes3 alsa-utils libXtst6 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire +Recommends: libayatana-appindicator3-1 xdotool # https://docs.fedoraproject.org/en-US/packaging-guidelines/Scriptlets/ diff --git a/res/rpm.spec b/res/rpm.spec index 67c7abe36..26c497121 100644 --- a/res/rpm.spec +++ b/res/rpm.spec @@ -5,8 +5,8 @@ Summary: RPM package License: GPL-3.0 URL: https://rustdesk.com Vendor: rustdesk -Requires: gtk3 libxcb libxdo libXfixes alsa-lib libva2 pam gstreamer1-plugins-base -Recommends: libayatana-appindicator-gtk3 +Requires: gtk3 libxcb libXfixes alsa-lib libva2 pam gstreamer1-plugins-base +Recommends: libayatana-appindicator-gtk3 libxdo # https://docs.fedoraproject.org/en-US/packaging-guidelines/Scriptlets/ diff --git a/src/platform/linux.rs b/src/platform/linux.rs index c546673eb..382af72cf 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -6,29 +6,26 @@ use hbb_common::{ anyhow::anyhow, bail, config::{keys::OPTION_ALLOW_LINUX_HEADLESS, Config}, - libc::{c_char, c_int, c_long, c_void}, + libc::{c_char, c_int, c_long, c_uint, c_void}, log, message_proto::{DisplayInfo, Resolution}, regex::{Captures, Regex}, users::{get_user_by_name, os::unix::UserExt}, }; +use libxdo_sys::{self, xdo_t, Window}; use std::{ cell::RefCell, ffi::{OsStr, OsString}, path::{Path, PathBuf}, process::{Child, Command}, string::String, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, + sync::atomic::{AtomicBool, Ordering}, + sync::Arc, time::{Duration, Instant}, }; use terminfo::{capability as cap, Database}; use wallpaper; -type Xdo = *const c_void; - pub const PA_SAMPLE_RATE: u32 = 48000; static mut UNMODIFIED: bool = true; @@ -86,35 +83,20 @@ lazy_static::lazy_static! { } thread_local! { - static XDO: RefCell = RefCell::new(unsafe { xdo_new(std::ptr::null()) }); + // XDO context - created via libxdo-sys (which uses dynamic loading stub). + // If libxdo is not available, xdo will be null and xdo-based functions become no-ops. + static XDO: RefCell<*mut xdo_t> = RefCell::new({ + let xdo = unsafe { libxdo_sys::xdo_new(std::ptr::null()) }; + if xdo.is_null() { + log::warn!("Failed to create xdo context, xdo functions will be disabled"); + } else { + log::info!("xdo context created successfully"); + } + xdo + }); static DISPLAY: RefCell<*mut c_void> = RefCell::new(unsafe { XOpenDisplay(std::ptr::null())}); } -extern "C" { - fn xdo_get_mouse_location( - xdo: Xdo, - x: *mut c_int, - y: *mut c_int, - screen_num: *mut c_int, - ) -> c_int; - fn xdo_move_mouse(xdo: Xdo, x: c_int, y: c_int, screen: c_int) -> c_int; - fn xdo_new(display: *const c_char) -> Xdo; - fn xdo_get_active_window(xdo: Xdo, window: *mut *mut c_void) -> c_int; - fn xdo_get_window_location( - xdo: Xdo, - window: *mut c_void, - x: *mut c_int, - y: *mut c_int, - screen_num: *mut c_int, - ) -> c_int; - fn xdo_get_window_size( - xdo: Xdo, - window: *mut c_void, - width: *mut c_int, - height: *mut c_int, - ) -> c_int; -} - #[link(name = "X11")] extern "C" { fn XOpenDisplay(display_name: *const c_char) -> *mut c_void; @@ -160,14 +142,19 @@ fn sleep_millis(millis: u64) { pub fn get_cursor_pos() -> Option<(i32, i32)> { let mut res = None; XDO.with(|xdo| { - if let Ok(xdo) = xdo.try_borrow_mut() { + if let Ok(xdo) = xdo.try_borrow() { if xdo.is_null() { return; } let mut x: c_int = 0; let mut y: c_int = 0; unsafe { - xdo_get_mouse_location(*xdo, &mut x as _, &mut y as _, std::ptr::null_mut()); + libxdo_sys::xdo_get_mouse_location( + *xdo as *const _, + &mut x as _, + &mut y as _, + std::ptr::null_mut(), + ); } res = Some((x, y)); } @@ -178,14 +165,14 @@ pub fn get_cursor_pos() -> Option<(i32, i32)> { pub fn set_cursor_pos(x: i32, y: i32) -> bool { let mut res = false; XDO.with(|xdo| { - match xdo.try_borrow_mut() { + match xdo.try_borrow() { Ok(xdo) => { if xdo.is_null() { log::debug!("set_cursor_pos: xdo is null"); return; } unsafe { - let ret = xdo_move_mouse(*xdo, x, y, 0); + let ret = libxdo_sys::xdo_move_mouse(*xdo as *const _, x, y, 0); if ret != 0 { log::debug!( "set_cursor_pos: xdo_move_mouse failed with code {} for coordinates ({}, {})", @@ -230,22 +217,22 @@ pub fn reset_input_cache() {} pub fn get_focused_display(displays: Vec) -> Option { let mut res = None; XDO.with(|xdo| { - if let Ok(xdo) = xdo.try_borrow_mut() { + if let Ok(xdo) = xdo.try_borrow() { if xdo.is_null() { return; } let mut x: c_int = 0; let mut y: c_int = 0; - let mut width: c_int = 0; - let mut height: c_int = 0; - let mut window: *mut c_void = std::ptr::null_mut(); + let mut width: c_uint = 0; + let mut height: c_uint = 0; + let mut window: Window = 0; unsafe { - if xdo_get_active_window(*xdo, &mut window) != 0 { + if libxdo_sys::xdo_get_active_window(*xdo as *const _, &mut window) != 0 { return; } - if xdo_get_window_location( - *xdo, + if libxdo_sys::xdo_get_window_location( + *xdo as *const _, window, &mut x as _, &mut y as _, @@ -254,11 +241,17 @@ pub fn get_focused_display(displays: Vec) -> Option { { return; } - if xdo_get_window_size(*xdo, window, &mut width as _, &mut height as _) != 0 { + if libxdo_sys::xdo_get_window_size( + *xdo as *const _, + window, + &mut width, + &mut height, + ) != 0 + { return; } - let center_x = x + width / 2; - let center_y = y + height / 2; + let center_x = x + (width / 2) as c_int; + let center_y = y + (height / 2) as c_int; res = displays.iter().position(|d| { center_x >= d.x && center_x < d.x + d.width @@ -497,7 +490,10 @@ fn get_all_term_values(uid: &str) -> Vec { let Ok(cmdline) = std::fs::read(&cmdline_path) else { continue; }; - let exe_end = cmdline.iter().position(|&b| b == 0).unwrap_or(cmdline.len()); + let exe_end = cmdline + .iter() + .position(|&b| b == 0) + .unwrap_or(cmdline.len()); let exe_str = String::from_utf8_lossy(&cmdline[..exe_end]); if !re.is_match(&exe_str) { continue; From 43b39102a413d2b891528aff2fc88508ccaa2500 Mon Sep 17 00:00:00 2001 From: solokot Date: Thu, 22 Jan 2026 09:12:26 +0300 Subject: [PATCH 093/277] Update ru.rs (#14099) --- src/lang/ru.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index d9a7f15b7..74c3f1358 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -737,7 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", "Нажмите {} для выхода."), ("rel-mouse-permission-lost-tip", "Разрешение на использование клавиатуры отменено. Режим относительного перемещения мыши отключён."), ("Changelog", "Журнал изменений"), - ("keep-awake-during-outgoing-sessions-label", ""), - ("keep-awake-during-incoming-sessions-label", ""), + ("keep-awake-during-outgoing-sessions-label", "Не отключать экран во время исходящих сеансов"), + ("keep-awake-during-incoming-sessions-label", "Не отключать экран во время входящих сеансов"), ].iter().cloned().collect(); } From 341eb0c6714f714213bfcfad4f84b20b7aed8ae1 Mon Sep 17 00:00:00 2001 From: bilimiyorum <131397022+bilimiyorum@users.noreply.github.com> Date: Thu, 22 Jan 2026 09:13:38 +0300 Subject: [PATCH 094/277] Updated tr.rs (#14100) New string entries Minor typo corrections --- src/lang/tr.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lang/tr.rs b/src/lang/tr.rs index f81bfdca7..45c8b79df 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -309,7 +309,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Map mode", "Haritalama modu"), ("Translate mode", "Çeviri modu"), ("Use permanent password", "Kalıcı şifre kullan"), - ("Use both passwords", "İki şifreyide kullan"), + ("Use both passwords", "İki şifreyi de kullan"), ("Set permanent password", "Kalıcı şifre oluştur"), ("Enable remote restart", "Uzaktan yeniden başlatmayı aktif et"), ("Restart remote device", "Uzaktaki cihazı yeniden başlat"), @@ -366,7 +366,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop session recording", "Oturum kaydını sonlandır"), ("Enable recording session", "Kayıt Oturumunu Aktif Et"), ("Enable LAN discovery", "Yerel Ağ Keşfine İzin Ver"), - ("Deny LAN discovery", "Yerl Ağ Keşfine İzin Verme"), + ("Deny LAN discovery", "Yerel Ağ Keşfine İzin Verme"), ("Write a message", "Bir mesaj yazın"), ("Prompt", "İstem"), ("Please wait for confirmation of UAC...", "UAC onayı için lütfen bekleyiniz..."), @@ -568,7 +568,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input_source_1_tip", "Giriş kaynağı 1"), ("input_source_2_tip", "Giriş kaynağı 2"), ("Swap control-command key", "Kontrol-komut tuşunu değiştir"), - ("swap-left-right-mouse", "sol-sağ fareyi değiştir"), + ("swap-left-right-mouse", "Sol-sağ fare tuşlarını değiştir"), ("2FA code", "2FA kodu"), ("More", "Daha"), ("enable-2fa-title", "İki faktörlü kimlik doğrulamayı etkinleştir"), @@ -737,7 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", "Göreli fare modundan çıkmak için {}"), ("rel-mouse-permission-lost-tip", "Göreli fare izinleri geçerli değil"), ("Changelog", "Değişiklik Günlüğü"), - ("keep-awake-during-outgoing-sessions-label", ""), - ("keep-awake-during-incoming-sessions-label", ""), + ("keep-awake-during-outgoing-sessions-label", "Giden oturumlar süresince ekranı açık tut"), + ("keep-awake-during-incoming-sessions-label", "Gelen oturumlar süresince ekranı açık tut"), ].iter().cloned().collect(); } From 087eb55299116abe5369f1ecd843b1204367b220 Mon Sep 17 00:00:00 2001 From: Yavuz Selim YAZICI <95548778+yavuzyazici@users.noreply.github.com> Date: Thu, 22 Jan 2026 09:15:14 +0300 Subject: [PATCH 095/277] Update tr.rs, Missing Turkish translations added (#14103) * Update tr.rs * Update tr.rs --------- Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> --- src/lang/tr.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 45c8b79df..fdb5d0322 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -49,7 +49,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Mute", "Sustur"), ("Build Date", "Yapım Tarihi"), ("Version", "Sürüm"), - ("Home", ""), + ("Home", "Anasayfa"), ("Audio Input", "Ses Girişi"), ("Enhancements", "Geliştirmeler"), ("Hardware Codec", "Donanımsal Codec"), @@ -598,7 +598,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("no_need_privacy_mode_no_physical_displays_tip", "Fiziksel ekran yok, gizlilik modunu kullanmaya gerek yok."), ("Follow remote cursor", "Uzak imleci takip et"), ("Follow remote window focus", "Uzak pencere odağını takip et"), - ("default_proxy_tip", ""), + ("default_proxy_tip", "Varsayılan protokol ve port Socks5 ve 1080'dir."), ("no_audio_input_device_tip", "Varsayılan protokol ve port, Socks5 ve 1080'dir"), ("Incoming", "Gelen"), ("Outgoing", "Giden"), @@ -696,8 +696,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable camera", "Kamerayı etkinleştir"), ("No cameras", "Kamera yok"), ("view_camera_unsupported_tip", "Uzak cihaz, kameranın görüntülenmesini desteklemiyor."), - ("Terminal", ""), - ("Enable terminal", ""), + ("Terminal", "Terminal"), + ("Enable terminal", "Terminali etkinleştir"), ("New tab", "Yeni sekme"), ("Keep terminal sessions on disconnect", "Bağlantı kesildiğinde terminal oturumlarını açık tut"), ("Terminal (Run as administrator)", "Terminal (Yönetici olarak çalıştır)"), @@ -737,7 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", "Göreli fare modundan çıkmak için {}"), ("rel-mouse-permission-lost-tip", "Göreli fare izinleri geçerli değil"), ("Changelog", "Değişiklik Günlüğü"), - ("keep-awake-during-outgoing-sessions-label", "Giden oturumlar süresince ekranı açık tut"), - ("keep-awake-during-incoming-sessions-label", "Gelen oturumlar süresince ekranı açık tut"), + ("keep-awake-during-outgoing-sessions-label", "Giden oturumlar süresince ekranı açık tutun"), + ("keep-awake-during-incoming-sessions-label", "Gelen oturumlar süresince ekranı açık tutun"), ].iter().cloned().collect(); } From e4b06dadf5e1a01e5b056a1094ace76e5206cf49 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 23 Jan 2026 15:05:11 +0800 Subject: [PATCH 096/277] auto retry on offline when already connected (#14124) When controlled peer is reconnecting after signout/switch user, auto retry for 30s (matches server's peer offline threshold) instead of immediately showing "Remote desktop is offline" error. Ref: https://github.com/rustdesk/rustdesk/discussions/14048 Signed-off-by: 21pages --- flutter/lib/models/model.dart | 43 +++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 578ba3ce3..7a3f98377 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -120,6 +120,7 @@ class FfiModel with ChangeNotifier { late VirtualMouseMode virtualMouseMode; Timer? _timer; var _reconnects = 1; + DateTime? _offlineReconnectStartTime; bool _viewOnly = false; bool _showMyCursor = false; WeakReference parent; @@ -783,7 +784,8 @@ class FfiModel with ChangeNotifier { } } - Future updateCurDisplay(SessionID sessionId, {updateCursorPos = false}) async { + Future updateCurDisplay(SessionID sessionId, + {updateCursorPos = false}) async { final newRect = displaysRect(); if (newRect == null) { return; @@ -939,11 +941,46 @@ class FfiModel with ChangeNotifier { showPrivacyFailedDialog( sessionId, type, title, text, link, hasRetry, dialogManager); } else { - final hasRetry = evt['hasRetry'] == 'true'; + var hasRetry = evt['hasRetry'] == 'true'; + if (!hasRetry) { + hasRetry = shouldAutoRetryOnOffline(type, title, text); + } showMsgBox(sessionId, type, title, text, link, hasRetry, dialogManager); } } + /// Auto-retry check for "Remote desktop is offline" error. + /// returns true to auto-retry, false otherwise. + bool shouldAutoRetryOnOffline( + String type, + String title, + String text, + ) { + if (type == 'error' && + title == 'Connection Error' && + text == 'Remote desktop is offline' && + _pi.isSet.isTrue) { + // Auto retry for ~30s (server's peer offline threshold) when controlled peer's account changes + // (e.g., signout, switch user, login into OS) causes temporary offline via websocket/tcp connection. + // The actual wait may exceed 30s (e.g., 20s elapsed + 16s next retry = 36s), which is acceptable + // since the controlled side reconnects quickly after account changes. + // Uses time-based check instead of _reconnects count because user can manually retry. + // https://github.com/rustdesk/rustdesk/discussions/14048 + if (_offlineReconnectStartTime == null) { + // First offline, record time and start retry + _offlineReconnectStartTime = DateTime.now(); + return true; + } else { + final elapsed = + DateTime.now().difference(_offlineReconnectStartTime!).inSeconds; + if (elapsed < 30) { + return true; + } + } + } + return false; + } + handleToast(Map evt, SessionID sessionId, String peerId) { final type = evt['type'] ?? 'info'; final text = evt['text'] ?? ''; @@ -1001,6 +1038,7 @@ class FfiModel with ChangeNotifier { _reconnects *= 2; } else { _reconnects = 1; + _offlineReconnectStartTime = null; } } @@ -1323,6 +1361,7 @@ class FfiModel with ChangeNotifier { } if (displays.isNotEmpty) { _reconnects = 1; + _offlineReconnectStartTime = null; waitForFirstImage.value = true; isRefreshing = false; } From ceffcce20e927636b930af1b2accdbf7048a1de6 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Fri, 23 Jan 2026 19:09:33 +0800 Subject: [PATCH 097/277] =?UTF-8?q?=20fix=20hide-tray=3DY=20causing=20The?= =?UTF-8?q?=20application=20=E2=80=9CRustDesk.app=E2=80=9D=20is=20not=20op?= =?UTF-8?q?en=20anymore.=20=20https://github.com/rustdesk/rustdesk/discuss?= =?UTF-8?q?ions/10210=20(#14127)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/tray.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/tray.rs b/src/tray.rs index f36da2cec..8ab4e3ecb 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -10,12 +10,6 @@ use std::time::Duration; pub fn start_tray() { if crate::ui_interface::get_builtin_option(hbb_common::config::keys::OPTION_HIDE_TRAY) == "Y" { - #[cfg(target_os = "macos")] - { - loop { - std::thread::sleep(std::time::Duration::from_secs(1)); - } - } #[cfg(not(target_os = "macos"))] { return; @@ -129,6 +123,11 @@ fn make_tray() -> hbb_common::ResultType<()> { ); if let tao::event::Event::NewEvents(tao::event::StartCause::Init) = event { + // for fixing https://github.com/rustdesk/rustdesk/discussions/10210#discussioncomment-14600745 + // so we start tray, but not to show it + if crate::ui_interface::get_builtin_option(hbb_common::config::keys::OPTION_HIDE_TRAY) == "Y" { + return; + } // We create the icon once the event loop is actually running // to prevent issues like https://github.com/tauri-apps/tray-icon/issues/90 let tray = TrayIconBuilder::new() From 0dc3c12aa5b6e9ce4b2c2a20add27814891988f0 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Sat, 24 Jan 2026 05:50:18 +0100 Subject: [PATCH 098/277] Update de.rs (#14108) --- src/lang/de.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 86545b3df..f77c3cc97 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -737,7 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", "Drücken Sie {} zum Beenden."), ("rel-mouse-permission-lost-tip", "Die Tastaturberechtigung wurde widerrufen. Der relative Mausmodus wurde deaktiviert."), ("Changelog", "Änderungsprotokoll"), - ("keep-awake-during-outgoing-sessions-label", ""), - ("keep-awake-during-incoming-sessions-label", ""), + ("keep-awake-during-outgoing-sessions-label", "Bildschirm während ausgehender Sitzungen aktiv halten"), + ("keep-awake-during-incoming-sessions-label", "Bildschirm während eingehender Sitzungen aktiv halten"), ].iter().cloned().collect(); } From 6b334f297770cb40d2092643df973fa2473f7674 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?VenusGirl=E2=9D=A4?= Date: Sun, 25 Jan 2026 17:37:34 +0900 Subject: [PATCH 099/277] Update ko.rs (#14110) Update Korean --- src/lang/ko.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 1c3200629..21fcb7661 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -737,7 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", "종료하려면 {}을(를) 누르세요."), ("rel-mouse-permission-lost-tip", "키보드 권한이 취소되었습니다. 상대 마우스 모드가 비활성화되었습니다."), ("Changelog", "변경 기록"), - ("keep-awake-during-outgoing-sessions-label", ""), - ("keep-awake-during-incoming-sessions-label", ""), + ("keep-awake-during-outgoing-sessions-label", "발신 세션 중 화면 켜짐 유지"), + ("keep-awake-during-incoming-sessions-label", "수신 세션 중 화면 켜짐 유지"), ].iter().cloned().collect(); } From 1f35830570372c89cb33128599f28ae5403a5d96 Mon Sep 17 00:00:00 2001 From: hatterp Date: Mon, 26 Jan 2026 07:11:41 +0100 Subject: [PATCH 100/277] Update pl.rs (#14112) updated PL translation --- src/lang/pl.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 9f71948ab..6ce5b98fa 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -737,7 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", "Aby wyłączyć tryb przechwytywania myszy, naciśnij {}"), ("rel-mouse-permission-lost-tip", "Utracono uprawnienia do trybu przechwytywania myszy"), ("Changelog", "Dziennik zmian"), - ("keep-awake-during-outgoing-sessions-label", ""), - ("keep-awake-during-incoming-sessions-label", ""), + ("keep-awake-during-outgoing-sessions-label", "Utrzymuj urządzenie w stanie aktywnym podczas sesji wychodzących"), + ("keep-awake-during-incoming-sessions-label", "Utrzymuj urządzenie w stanie aktywnym podczas sesji przychodzących"), ].iter().cloned().collect(); } From 204e81a700a1436d0b12900b7d9d7a0cd2ef2ce8 Mon Sep 17 00:00:00 2001 From: bilimiyorum <131397022+bilimiyorum@users.noreply.github.com> Date: Mon, 26 Jan 2026 09:11:58 +0300 Subject: [PATCH 101/277] Updated tr.rs (#14115) Translation improvements have been made. --- src/tr.rs | 743 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 743 insertions(+) create mode 100644 src/tr.rs diff --git a/src/tr.rs b/src/tr.rs new file mode 100644 index 000000000..08f8de37f --- /dev/null +++ b/src/tr.rs @@ -0,0 +1,743 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Durum"), + ("Your Desktop", "Sizin Masaüstünüz"), + ("desk_tip", "Masaüstünüze bu ID ve parola ile erişilebilir"), + ("Password", "Parola"), + ("Ready", "Hazır"), + ("Established", "Bağlantı sağlandı"), + ("connecting_status", "Bağlanılıyor "), + ("Enable service", "Servisi aktif et"), + ("Start service", "Servisi başlat"), + ("Service is running", "Servis çalışıyor"), + ("Service is not running", "Servis çalışmıyor"), + ("not_ready_status", "Hazır değil. Bağlantınızı kontrol edin"), + ("Control Remote Desktop", "Uzak Masaüstünü Denetle"), + ("Transfer file", "Dosya transferi"), + ("Connect", "Bağlan"), + ("Recent sessions", "Son oturumlar"), + ("Address book", "Adres Defteri"), + ("Confirmation", "Onayla"), + ("TCP tunneling", "TCP tünelleri"), + ("Remove", "Kaldır"), + ("Refresh random password", "Yeni rastgele parola oluştur"), + ("Set your own password", "Kendi parolanı oluştur"), + ("Enable keyboard/mouse", "Klavye ve Fareye izin ver"), + ("Enable clipboard", "Kopyalanan geçici veriye izin ver"), + ("Enable file transfer", "Dosya Transferine izin ver"), + ("Enable TCP tunneling", "TCP Tüneline izin ver"), + ("IP Whitelisting", "İzinli IP listesi"), + ("ID/Relay Server", "ID/Relay Sunucusu"), + ("Import server config", "Sunucu ayarlarını içe aktar"), + ("Export Server Config", "Sunucu Yapılandırmasını Dışa Aktar"), + ("Import server configuration successfully", "Sunucu ayarları başarıyla içe aktarıldı"), + ("Export server configuration successfully", "Sunucu yapılandırmasını başarıyla dışa aktar"), + ("Invalid server configuration", "Geçersiz sunucu ayarı"), + ("Clipboard is empty", "Kopyalanan geçici veri boş"), + ("Stop service", "Servisi Durdur"), + ("Change ID", "ID Değiştir"), + ("Your new ID", "Yeni ID'niz"), + ("length %min% to %max%", "uzunluk %min% ila %max%"), + ("starts with a letter", "bir harfle başlar"), + ("allowed characters", "izin verilen karakterler"), + ("id_change_tip", "Yalnızca a-z, A-Z, 0-9, - (dash) ve _ (alt çizgi) karakterlerini kullanabilirsiniz. İlk karakter a-z veya A-Z olmalıdır. Uzunluk 6 ile 16 karakter arasında olmalıdır."), + ("Website", "Website"), + ("About", "Hakkında"), + ("Slogan_tip", "Bu kaotik dünyada gönülden yapıldı!"), + ("Privacy Statement", "Gizlilik Beyanı"), + ("Mute", "Sustur"), + ("Build Date", "Derleme Tarihi"), + ("Version", "Sürüm"), + ("Home", "Ana Sayfa"), + ("Audio Input", "Ses Girişi"), + ("Enhancements", "Geliştirmeler"), + ("Hardware Codec", "Donanımsal Codec"), + ("Adaptive bitrate", "Uyarlanabilir Bit Hızı"), + ("ID Server", "ID Sunucu"), + ("Relay Server", "Relay Sunucu"), + ("API Server", "API Sunucu"), + ("invalid_http", "http:// veya https:// ile başlamalıdır"), + ("Invalid IP", "Geçersiz IP adresi"), + ("Invalid format", "Hatalı Format"), + ("server_not_support", "Henüz sunucu tarafından desteklenmiyor"), + ("Not available", "Erişilebilir değil"), + ("Too frequent", "Çok sık"), + ("Cancel", "İptal"), + ("Skip", "Atla"), + ("Close", "Kapat"), + ("Retry", "Tekrar Dene"), + ("OK", "Tamam"), + ("Password Required", "Parola Gerekli"), + ("Please enter your password", "Lütfen parolanızı giriniz"), + ("Remember password", "Parolayı hatırla"), + ("Wrong Password", "Hatalı parola"), + ("Do you want to enter again?", "Tekrar giriş yapmak ister misiniz?"), + ("Connection Error", "Bağlantı Hatası"), + ("Error", "Hata"), + ("Reset by the peer", "Eş tarafından sıfırlandı"), + ("Connecting...", "Bağlanılıyor..."), + ("Connection in progress. Please wait.", "Bağlantı sağlanıyor. Lütfen bekleyiniz."), + ("Please try 1 minute later", "Lütfen 1 dakika sonra tekrar deneyiniz"), + ("Login Error", "Giriş Hatalı"), + ("Successful", "Başarılı"), + ("Connected, waiting for image...", "Bağlandı. Görüntü bekleniyor..."), + ("Name", "Ad"), + ("Type", "Tip"), + ("Modified", "Değiştirildi"), + ("Size", "Boyut"), + ("Show Hidden Files", "Gizli Dosyaları Göster"), + ("Receive", "Al"), + ("Send", "Gönder"), + ("Refresh File", "Dosyayı yenile"), + ("Local", "Yerel"), + ("Remote", "Uzak"), + ("Remote Computer", "Uzak Bilgisayar"), + ("Local Computer", "Yerel Bilgisayar"), + ("Confirm Delete", "Silmeyi Onayla"), + ("Delete", "Sil"), + ("Properties", "Özellikler"), + ("Multi Select", "Çoklu Seçim"), + ("Select All", "Tümünü Seç"), + ("Unselect All", "Tüm Seçimi Kaldır"), + ("Empty Directory", "Boş Klasör"), + ("Not an empty directory", "Klasör boş değil"), + ("Are you sure you want to delete this file?", "Bu dosyayı silmek istediğinize emin misiniz?"), + ("Are you sure you want to delete this empty directory?", "Bu boş klasörü silmek istediğinize emin misiniz?"), + ("Are you sure you want to delete the file of this directory?", "Bu klasördeki dosyayı silmek istediğinize emin misiniz?"), + ("Do this for all conflicts", "Bunu tüm çakışmalar için yap"), + ("This is irreversible!", "Bu işlem geri döndürülemez!"), + ("Deleting", "Siliniyor"), + ("files", "dosyalar"), + ("Waiting", "Bekleniyor"), + ("Finished", "Tamamlandı"), + ("Speed", "Hız"), + ("Custom Image Quality", "Özel Görüntü Kalitesi"), + ("Privacy mode", "Gizlilik modu"), + ("Block user input", "Kullanıcı girişini engelle"), + ("Unblock user input", "Kullanı girişine izin ver"), + ("Adjust Window", "Pencereyi Ayarla"), + ("Original", "Orjinal"), + ("Shrink", "Küçült"), + ("Stretch", "Uzat"), + ("Scrollbar", "Kaydırma çubuğu"), + ("ScrollAuto", "Otomatik Kaydır"), + ("Good image quality", "İyi görüntü kalitesi"), + ("Balanced", "Dengelenmiş"), + ("Optimize reaction time", "Tepki süresini optimize et"), + ("Custom", "Özel"), + ("Show remote cursor", "Uzaktaki fare imlecini göster"), + ("Show quality monitor", "Kalite monitörünü göster"), + ("Disable clipboard", "Hafızadaki kopyalanmışları engelle"), + ("Lock after session end", "Bağlantıdan sonra kilitle"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del Ekle"), + ("Insert Lock", "Kilit Ekle"), + ("Refresh", "Yenile"), + ("ID does not exist", "ID bulunamadı"), + ("Failed to connect to rendezvous server", "ID oluşturma sunucusuna bağlanılamadı"), + ("Please try later", "Daha sonra tekrar deneyiniz"), + ("Remote desktop is offline", "Uzak masaüstü kapalı"), + ("Key mismatch", "Anahtar uyumlu değil"), + ("Timeout", "Zaman aşımı"), + ("Failed to connect to relay server", "Relay sunucusuna bağlanılamadı"), + ("Failed to connect via rendezvous server", "ID oluşturma sunucusuna bağlanılamadı"), + ("Failed to connect via relay server", "Aktarma sunucusuna bağlanılamadı"), + ("Failed to make direct connection to remote desktop", "Uzak masaüstüne doğrudan bağlantı kurulamadı"), + ("Set Password", "Parola ayarla"), + ("OS Password", "İşletim Sistemi Parolası"), + ("install_tip", "Kullanıcı Hesabı Denetimi nedeniyle, RustDesk bir uzak masaüstü olarak düzgün çalışmayabilir. Bu sorunu önlemek için, RustDesk'i sistem seviyesinde kurmak için aşağıdaki butona tıklayın."), + ("Click to upgrade", "Yükseltmek için tıklayınız"), + ("Configure", "Ayarla"), + ("config_acc", "Masaüstünüzü dışarıdan kontrol etmek için RustDesk'e \"Erişilebilirlik\""), + ("config_screen", "Masaüstünüzü dışarıdan kontrol etmek için RustDesk'e \"Ekran Kaydı\" iznini vermeniz gerekir."), + ("Installing ...", "Yükleniyor ..."), + ("Install", "Yükle"), + ("Installation", "Kurulum"), + ("Installation Path", "Kurulacak olan konum"), + ("Create start menu shortcuts", "Başlangıca kısayol oluştur"), + ("Create desktop icon", "Masaüstüne kısayol oluştur"), + ("agreement_tip", "Kurulumu başlatarak, lisans sözleşmesinin şartlarını kabul etmiş olursunuz."), + ("Accept and Install", "Kabul Et ve Yükle"), + ("End-user license agreement", "Son kullanıcı lisans anlaşması"), + ("Generating ...", "Oluşturuluyor..."), + ("Your installation is lower version.", "Kurulumunuz alt sürümdür."), + ("not_close_tcp_tip", "Tüneli kullanırken bu pencereyi kapatmayın"), + ("Listening ...", "Dinleniyor..."), + ("Remote Host", "Uzak Sunucu"), + ("Remote Port", "Uzak Port"), + ("Action", "Eylem"), + ("Add", "Ekle"), + ("Local Port", "Yerel Port"), + ("Local Address", "Yerel Adres"), + ("Change Local Port", "Yerel Port'u Değiştir"), + ("setup_server_tip", "Daha hızlı bağlantı için kendi sunucunuzu kurun"), + ("Too short, at least 6 characters.", "Çok kısa en az 6 karakter gerekli."), + ("The confirmation is not identical.", "Doğrulama yapılamadı."), + ("Permissions", "İzinler"), + ("Accept", "Kabul Et"), + ("Dismiss", "Reddet"), + ("Disconnect", "Bağlanıyı kes"), + ("Enable file copy and paste", "Dosya kopyalamaya ve yapıştırmaya izin ver"), + ("Connected", "Bağlandı"), + ("Direct and encrypted connection", "Doğrudan ve şifreli bağlantı"), + ("Relayed and encrypted connection", "Aktarmalı ve şifreli bağlantı"), + ("Direct and unencrypted connection", "Doğrudan ve şifrelenmemiş bağlantı"), + ("Relayed and unencrypted connection", "Aktarmalı ve şifrelenmemiş bağlantı"), + ("Enter Remote ID", "Uzak ID'yi Girin"), + ("Enter your password", "Parolanızı girin"), + ("Logging in...", "Giriş yapılıyor..."), + ("Enable RDP session sharing", "RDP oturum paylaşımını etkinleştir"), + ("Auto Login", "Otomatik giriş"), + ("Enable direct IP access", "Doğrudan IP Erişimini Etkinleştir"), + ("Rename", "Yeniden adlandır"), + ("Space", "Boşluk"), + ("Create desktop shortcut", "Masaüstü kısayolu oluşturun"), + ("Change Path", "Yolu değiştir"), + ("Create Folder", "Klasör oluşturun"), + ("Please enter the folder name", "Lütfen klasör adını girin"), + ("Fix it", "Düzenle"), + ("Warning", "Uyarı"), + ("Login screen using Wayland is not supported", "Wayland kullanan giriş ekranı desteklenmiyor"), + ("Reboot required", "Yeniden başlatma gerekli"), + ("Unsupported display server", "Desteklenmeyen görüntü sunucusu"), + ("x11 expected", "x11 bekleniyor"), + ("Port", "Port"), + ("Settings", "Ayarlar"), + ("Username", "Kullanıcı Adı"), + ("Invalid port", "Geçersiz port"), + ("Closed manually by the peer", "Eş tarafından manuel olarak kapatıldı"), + ("Enable remote configuration modification", "Uzaktan yapılandırma değişikliğini etkinleştir"), + ("Run without install", "Yüklemeden çalıştır"), + ("Connect via relay", "Aktarmalı üzerinden bağlan"), + ("Always connect via relay", "Her zaman aktarmalı üzerinden bağlan"), + ("whitelist_tip", "Bu masaüstüne yalnızca yetkili IP adresleri bağlanabilir"), + ("Login", "Giriş yap"), + ("Verify", "Doğrula"), + ("Remember me", "Beni hatırla"), + ("Trust this device", "Bu cihaza güvenin"), + ("Verification code", "Doğrulama kodu"), + ("verification_tip", "doğrulama tipi"), + ("Logout", "Çıkış yap"), + ("Tags", "Etiketler"), + ("Search ID", "ID Arama"), + ("whitelist_sep", "Virgül, noktalı virgül, boşluk veya yeni satır ile ayrılmış"), + ("Add ID", "ID Ekle"), + ("Add Tag", "Etiket Ekle"), + ("Unselect all tags", "Tüm etiketlerin seçimini kaldır"), + ("Network error", "Bağlantı hatası"), + ("Username missed", "Kullanıcı adı boş"), + ("Password missed", "Parola boş"), + ("Wrong credentials", "Yanlış kimlik bilgileri"), + ("The verification code is incorrect or has expired", "Doğrulama kodu hatalı veya süresi dolmuş"), + ("Edit Tag", "Etiketi düzenle"), + ("Forget Password", "Parolayı Unut"), + ("Favorites", "Favoriler"), + ("Add to Favorites", "Favorilere ekle"), + ("Remove from Favorites", "Favorilerden çıkar"), + ("Empty", "Boş"), + ("Invalid folder name", "Geçersiz klasör adı"), + ("Socks5 Proxy", "Socks5 Proxy"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"), + ("Discovered", "Keşfedilenler"), + ("install_daemon_tip", "Başlangıçta başlamak için sistem hizmetini yüklemeniz gerekir."), + ("Remote ID", "Uzak ID"), + ("Paste", "Yapıştır"), + ("Paste here?", "Buraya yapıştır?"), + ("Are you sure to close the connection?", "Bağlantıyı kapatmak istediğinize emin misiniz?"), + ("Download new version", "Yeni sürümü indir"), + ("Touch mode", "Dokunmatik mod"), + ("Mouse mode", "Fare modu"), + ("One-Finger Tap", "Tek Parmakla Dokunma"), + ("Left Mouse", "Sol Fare"), + ("One-Long Tap", "Tek-Uzun Dokunma"), + ("Two-Finger Tap", "İki-Parmak Dokunma"), + ("Right Mouse", "Sağ Fare"), + ("One-Finger Move", "Tek Parmakla Hareket"), + ("Double Tap & Move", "Çift Dokun ve Taşı"), + ("Mouse Drag", "Fare Sürükleme"), + ("Three-Finger vertically", "Dikey olarak üç parmak"), + ("Mouse Wheel", "Fare Tekerliği"), + ("Two-Finger Move", "İki Parmakla Hareket"), + ("Canvas Move", "Tuval Hareketi"), + ("Pinch to Zoom", "İki parmakla yakınlaştır"), + ("Canvas Zoom", "Tuval Yakınlaştırma"), + ("Reset canvas", "Tuvali sıfırla"), + ("No permission of file transfer", "Dosya aktarımı izni yok"), + ("Note", "Not"), + ("Connection", "Bağlantı"), + ("Share screen", "Ekranı Paylaş"), + ("Chat", "Mesajlaş"), + ("Total", "Toplam"), + ("items", "ögeler"), + ("Selected", "Seçildi"), + ("Screen Capture", "Ekran Görüntüsü"), + ("Input Control", "Giriş Kontrolü"), + ("Audio Capture", "Ses Yakalama"), + ("Do you accept?", "Kabul ediyor musun?"), + ("Open System Setting", "Sistem Ayarını Aç"), + ("How to get Android input permission?", "Android giriş izni nasıl alınır?"), + ("android_input_permission_tip1", "Uzak bir cihazın Android cihazınızı fare veya dokunma yoluyla kontrol edebilmesi için, RustDesk'in \"Erişilebilirlik\" özelliğini kullanmasına izin vermelisiniz."), + ("android_input_permission_tip2", "Sonraki sistem ayarları sayfasına gidin, [Yüklü Hizmetler]'i bulun ve erişin, [RustDesk Girişi] hizmetini etkinleştirin."), + ("android_new_connection_tip", "Yeni bir kontrol talebi alındı, cihazınızı kontrol etmesine izin verilsin mi."), + ("android_service_will_start_tip", "Ekran Yakalamanın etkinleştirilmesi, hizmeti otomatik olarak başlatacak ve diğer cihazların bu cihazdan bağlantı talep etmesine izin verecektir."), + ("android_stop_service_tip", "Hizmetin kapatılması, kurulan tüm bağlantıları otomatik olarak kapatacaktır."), + ("android_version_audio_tip", "Mevcut Android sürümü ses yakalamayı desteklemiyor, lütfen Android 10 veya sonraki bir sürüme yükseltin."), + ("android_start_service_tip", "Ekran paylaşım hizmetini başlatmak için [Hizmeti başlat] ögesine dokunun veya [Ekran Görüntüsü] iznini etkinleştirin."), + ("android_permission_may_not_change_tip", "Kurulan bağlantılara ait izinler, yeniden bağlantı kurulana kadar anında değiştirilemez."), + ("Account", "Hesap"), + ("Overwrite", "Üzerine yaz"), + ("This file exists, skip or overwrite this file?", "Bu dosya var, bu dosya atlansın veya üzerine yazılsın mı?"), + ("Quit", "Çıkış"), + ("Help", "Yardım"), + ("Failed", "Arızalı"), + ("Succeeded", "başarılı"), + ("Someone turns on privacy mode, exit", "Birisi gizlilik modunu açarsa, çık"), + ("Unsupported", "desteklenmiyor"), + ("Peer denied", "eş reddedildi"), + ("Please install plugins", "Lütfen eklentileri yükleyin"), + ("Peer exit", "Eş çıkışı"), + ("Failed to turn off", "Kapatılamadı"), + ("Turned off", "Kapatıldı"), + ("Language", "Dil"), + ("Keep RustDesk background service", "RustDesk arka plan hizmetini sürdürün"), + ("Ignore Battery Optimizations", "Pil Optimizasyonlarını Yoksay"), + ("android_open_battery_optimizations_tip", "Bu özelliği devre dışı bırakmak istiyorsanız lütfen bir sonraki RustDesk uygulama ayarları sayfasına gidin, [Pil] ögesini bulun ve girin, [Sınırsız] ögesinin işaretini kaldırın"), + ("Start on boot", "Önyüklemede başla"), + ("Start the screen sharing service on boot, requires special permissions", "Ekran paylaşım hizmetini önyüklemede başlatmak için özel izinler gerekir"), + ("Connection not allowed", "Bağlantıya izin verilmedi"), + ("Legacy mode", "Eski mod"), + ("Map mode", "Haritalama modu"), + ("Translate mode", "Çeviri modu"), + ("Use permanent password", "Kalıcı parola kullan"), + ("Use both passwords", "İki parolayı da kullan"), + ("Set permanent password", "Kalıcı parola oluştur"), + ("Enable remote restart", "Uzaktan yeniden başlatmayı aktif et"), + ("Restart remote device", "Uzaktaki cihazı yeniden başlat"), + ("Are you sure you want to restart", "Yeniden başlatmak istediğine emin misin?"), + ("Restarting remote device", "Uzaktan yeniden başlatılıyor"), + ("remote_restarting_tip", "Uzak cihaz yeniden başlatılıyor, lütfen bu mesaj kutusunu kapatın ve bir süre sonra kalıcı parola ile yeniden bağlanın"), + ("Copied", "Kopyalandı"), + ("Exit Fullscreen", "Tam Ekrandan Çık"), + ("Fullscreen", "Tam Ekran"), + ("Mobile Actions", "Mobil İşlemler"), + ("Select Monitor", "Monitörü Seç"), + ("Control Actions", "Kontrol Eylemleri"), + ("Display Settings", "Görüntü Ayarları"), + ("Ratio", "Oran"), + ("Image Quality", "Görüntü Kalitesi"), + ("Scroll Style", "Kaydırma Stili"), + ("Show Toolbar", "Araç Çubuğunu Göster"), + ("Hide Toolbar", "Araç Çubuğunu Gizle"), + ("Direct Connection", "Doğrudan Bağlantı"), + ("Relay Connection", "Aktarmalı Bağlantı"), + ("Secure Connection", "Güvenli Bağlantı"), + ("Insecure Connection", "Güvenli Olmayan Bağlantı"), + ("Scale original", "Orijinal ölçekte"), + ("Scale adaptive", "Uyarlanabilir ölçekte"), + ("General", "Genel"), + ("Security", "Güvenlik"), + ("Theme", "Tema"), + ("Dark Theme", "Koyu Tema"), + ("Light Theme", "Açık Tema"), + ("Dark", "Koyu"), + ("Light", "Açık"), + ("Follow System", "Sisteme Uy"), + ("Enable hardware codec", "Donanımsal codec aktif et"), + ("Unlock Security Settings", "Güvenlik Ayarlarını Aç"), + ("Enable audio", "Sesi Aktif Et"), + ("Unlock Network Settings", "Ağ Ayarlarını Aç"), + ("Server", "Sunucu"), + ("Direct IP Access", "Doğrudan IP Erişimi"), + ("Proxy", "Vekil"), + ("Apply", "Uygula"), + ("Disconnect all devices?", "Tüm cihazların bağlantısı kesilsin mi?"), + ("Clear", "Temizle"), + ("Audio Input Device", "Ses Giriş Aygıtı"), + ("Use IP Whitelisting", "IP Beyaz Listeyi Kullan"), + ("Network", "Ağ"), + ("Pin Toolbar", "Araç Çubuğunu Sabitle"), + ("Unpin Toolbar", "Araç Çubuğunu Sabitlemeyi Kaldır"), + ("Recording", "Kaydediliyor"), + ("Directory", "Dizin"), + ("Automatically record incoming sessions", "Gelen oturumları otomatik olarak kaydet"), + ("Automatically record outgoing sessions", "Giden oturumları otomatik olarak kaydet"), + ("Change", "Değiştir"), + ("Start session recording", "Oturum kaydını başlat"), + ("Stop session recording", "Oturum kaydını sonlandır"), + ("Enable recording session", "Kayıt Oturumunu Aktif Et"), + ("Enable LAN discovery", "Yerel Ağ Keşfine İzin Ver"), + ("Deny LAN discovery", "Yerel Ağ Keşfine İzin Verme"), + ("Write a message", "Bir mesaj yazın"), + ("Prompt", "İstem"), + ("Please wait for confirmation of UAC...", "UAC onayı için lütfen bekleyiniz..."), + ("elevated_foreground_window_tip", "elevated_foreground_window_tip"), + ("Disconnected", "Bağlantı Kesildi"), + ("Other", "Diğer"), + ("Confirm before closing multiple tabs", "Çoklu sekmeleri kapatmadan önce onayla"), + ("Keyboard Settings", "Klavye Ayarları"), + ("Full Access", "Tam Erişim"), + ("Screen Share", "Ekran Paylaşımı"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland, Ubuntu 21.04 veya daha yüksek bir sürüm gerektirir."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland, linux dağıtımının daha yüksek bir sürümünü gerektirir. Lütfen X11 masaüstünü deneyin veya işletim sisteminizi değiştirin."), + ("JumpLink", "View"), + ("Please Select the screen to be shared(Operate on the peer side).", "Lütfen paylaşılacak ekranı seçiniz (Ekran tarafında çalıştırın)."), + ("Show RustDesk", "RustDesk'i Göster"), + ("This PC", "Bu PC"), + ("or", "veya"), + ("Continue with", "Bununla devam et"), + ("Elevate", "Yükseltme"), + ("Zoom cursor", "Yakınlaştırma imleci"), + ("Accept sessions via password", "Oturumları parola ile kabul etme"), + ("Accept sessions via click", "Tıklama yoluyla oturumları kabul edin"), + ("Accept sessions via both", "Her ikisi aracılığıyla oturumları kabul edin"), + ("Please wait for the remote side to accept your session request...", "Lütfen uzak tarafın oturum isteğinizi kabul etmesini bekleyin..."), + ("One-time Password", "Tek Kullanımlık Parola"), + ("Use one-time password", "Tek seferlik parola kullanın"), + ("One-time password length", "Tek seferlik parola uzunluğu"), + ("Request access to your device", "Cihazınıza erişim talep edin"), + ("Hide connection management window", "Bağlantı yönetimi penceresini gizle"), + ("hide_cm_tip", "Oturumları yalnızca parola ile kabul edebilir ve kalıcı parola kullanıyorsanız gizlemeye izin verin"), + ("wayland_experiment_tip", "Wayland desteği deneysel aşamada olduğundan, gerektiğinde X11'i kullanmanız önerilir"), + ("Right click to select tabs", "Sekmeleri seçmek için sağ tıklayın"), + ("Skipped", "Atlandı"), + ("Add to address book", "Adres Defterine Ekle"), + ("Group", "Grup"), + ("Search", "Ara"), + ("Closed manually by web console", "Web konsoluyla manuel olarak kapatıldı"), + ("Local keyboard type", "Yerel klavye türü"), + ("Select local keyboard type", "Yerel klavye türünü seçin"), + ("software_render_tip", "Linux altında Nvidia grafik kartı kullanıyorsanız ve uzak pencere bağlandıktan hemen sonra kapanıyorsa, açık kaynaklı Nouveau sürücüsüne geçmeyi ve yazılım renderleme seçeneğini seçmeyi deneyin. Yazılımı yeniden başlatmanız gerekebilir."), + ("Always use software rendering", "Her zaman yazılım renderleme kullan"), + ("config_input", "Uzaktaki masaüstünü klavye ile kontrol etmek için RustDesk'e \"Giriş İzleme\" izinleri vermelisiniz."), + ("config_microphone", "Uzaktan konuşmak için RustDesk'e \"Ses Kaydı\" izinleri vermelisiniz."), + ("request_elevation_tip", "Ayrıca, uzak tarafta biri varsa yükseltme isteğinde bulunabilirsiniz."), + ("Wait", "Bekle"), + ("Elevation Error", "Yükseltme Hatası"), + ("Ask the remote user for authentication", "Uzaktaki kullanıcıdan kimlik doğrulamasını isteyin"), + ("Choose this if the remote account is administrator", "Uzak hesap yönetici ise bunu seçin"), + ("Transmit the username and password of administrator", "Yönetici kullanıcı adı ve parolasını iletim yapın"), + ("still_click_uac_tip", "Uzaktaki kullanıcının çalışan RustDesk'in UAC penceresinde hala Tamam'ı tıklaması gerekmektedir."), + ("Request Elevation", "Yükseltme İsteği"), + ("wait_accept_uac_tip", "Lütfen uzaktaki kullanıcının UAC iletişim kutusunu kabul etmesini bekleyin."), + ("Elevate successfully", "Başarıyla yükseltildi"), + ("uppercase", "büyük harf"), + ("lowercase", "küçük harf"), + ("digit", "rakam"), + ("special character", "özel karakter"), + ("length>=8", "uzunluk>=8"), + ("Weak", "Zayıf"), + ("Medium", "Orta"), + ("Strong", "Güçlü"), + ("Switch Sides", "Tarafları Değiştir"), + ("Please confirm if you want to share your desktop?", "Masaüstünüzü paylaşmak isteyip istemediğinizi onaylayın?"), + ("Display", "Görüntüle"), + ("Default View Style", "Varsayılan Görünüm Stili"), + ("Default Scroll Style", "Varsayılan Kaydırma Stili"), + ("Default Image Quality", "Varsayılan Görüntü Kalitesi"), + ("Default Codec", "Varsayılan Kodlayıcı"), + ("Bitrate", "Bit Hızı"), + ("FPS", "FPS"), + ("Auto", "Otomatik"), + ("Other Default Options", "Diğer Varsayılan Seçenekler"), + ("Voice call", "Sesli görüşme"), + ("Text chat", "Metin sohbeti"), + ("Stop voice call", "Sesli görüşmeyi durdur"), + ("relay_hint_tip", "Doğrudan bağlanmak mümkün olmayabilir; aktarmalı bağlanmayı deneyebilirsiniz. Ayrıca, ilk denemenizde aktarma sunucusu kullanmak istiyorsanız ID'nin sonuna \"/r\" ekleyebilir veya son oturum kartındaki \"Her Zaman Aktarmalı Üzerinden Bağlan\" seçeneğini seçebilirsiniz."), + ("Reconnect", "Yeniden Bağlan"), + ("Codec", "Kodlayıcı"), + ("Resolution", "Çözünürlük"), + ("No transfers in progress", "Devam eden aktarımlar yok"), + ("Set one-time password length", "Bir seferlik parola uzunluğunu ayarla"), + ("RDP Settings", "RDP Ayarları"), + ("Sort by", "Sırala"), + ("New Connection", "Yeni Bağlantı"), + ("Restore", "Geri Yükle"), + ("Minimize", "Simge Durumuna Küçült"), + ("Maximize", "Büyüt"), + ("Your Device", "Cihazınız"), + ("empty_recent_tip", "Üzgünüz, henüz son oturum yok!\nYeni bir plan yapma zamanı."), + ("empty_favorite_tip", "Henüz favori cihazınız yok mu?\nBağlanacak ve favorilere eklemek için birini bulalım!"), + ("empty_lan_tip", "Hayır, henüz hiçbir cihaz bulamadık gibi görünüyor."), + ("empty_address_book_tip", "Üzgünüm, şu anda adres defterinizde kayıtlı cihaz yok gibi görünüyor."), + ("Empty Username", "Boş Kullanıcı Adı"), + ("Empty Password", "Boş Parola"), + ("Me", "Ben"), + ("identical_file_tip", "Bu dosya, cihazın dosyası ile aynıdır."), + ("show_monitors_tip", "Monitörleri araç çubuğunda göster"), + ("View Mode", "Görünüm Modu"), + ("login_linux_tip", "X masaüstü oturumu başlatmak için uzaktaki Linux hesabına giriş yapmanız gerekiyor"), + ("verify_rustdesk_password_tip", "RustDesk parolasını doğrulayın"), + ("remember_account_tip", "Bu hesabı hatırla"), + ("os_account_desk_tip", "Bu hesap, uzaktaki işletim sistemine giriş yapmak ve başsız masaüstü oturumunu etkinleştirmek için kullanılır."), + ("OS Account", "İşletim Sistemi Hesabı"), + ("another_user_login_title_tip", "Başka bir kullanıcı zaten oturum açtı"), + ("another_user_login_text_tip", "Bağlantıyı Kapat"), + ("xorg_not_found_title_tip", "Xorg bulunamadı"), + ("xorg_not_found_text_tip", "Lütfen Xorg'u yükleyin"), + ("no_desktop_title_tip", "Masaüstü mevcut değil"), + ("no_desktop_text_tip", "Lütfen GNOME masaüstünü yükleyin"), + ("No need to elevate", "Yükseltmeye gerek yok"), + ("System Sound", "Sistem Sesi"), + ("Default", "Varsayılan"), + ("New RDP", "Yeni RDP"), + ("Fingerprint", "Parmak İzi"), + ("Copy Fingerprint", "Parmak İzini Kopyala"), + ("no fingerprints", "parmak izi yok"), + ("Select a peer", "Bir cihaz seçin"), + ("Select peers", "Cihazları seçin"), + ("Plugins", "Eklentiler"), + ("Uninstall", "Kaldır"), + ("Update", "Güncelle"), + ("Enable", "Etkinleştir"), + ("Disable", "Devre Dışı Bırak"), + ("Options", "Seçenekler"), + ("resolution_original_tip", "Orijinal çözünürlük"), + ("resolution_fit_local_tip", "Yerel çözünürlüğe sığdır"), + ("resolution_custom_tip", "Özel çözünürlük"), + ("Collapse toolbar", "Araç çubuğunu daralt"), + ("Accept and Elevate", "Kabul Et ve Yükselt"), + ("accept_and_elevate_btn_tooltip", "Bağlantıyı kabul et ve UAC izinlerini yükselt."), + ("clipboard_wait_response_timeout_tip", "Kopyalama yanıtı için zaman aşımına uğradı."), + ("Incoming connection", "Gelen bağlantı"), + ("Outgoing connection", "Giden bağlantı"), + ("Exit", "Çıkış"), + ("Open", "Aç"), + ("logout_tip", "Çıkış yapmak istediğinizden emin misiniz?"), + ("Service", "Hizmet"), + ("Start", "Başlat"), + ("Stop", "Durdur"), + ("exceed_max_devices", "Yönetilen cihazların maksimum sayısına ulaştınız."), + ("Sync with recent sessions", "Son oturumlarla senkronize et"), + ("Sort tags", "Etiketleri sırala"), + ("Open connection in new tab", "Bağlantıyı yeni sekmede aç"), + ("Move tab to new window", "Sekmeyi yeni pencereye taşı"), + ("Can not be empty", "Boş olamaz"), + ("Already exists", "Zaten var"), + ("Change Password", "Parolayı Değiştir"), + ("Refresh Password", "Parolayı Yenile"), + ("ID", "Kimlik"), + ("Grid View", "Izgara Görünümü"), + ("List View", "Liste Görünümü"), + ("Select", "Seç"), + ("Toggle Tags", "Etiketleri Değiştir"), + ("pull_ab_failed_tip", "Adres defterini yenileyemedi"), + ("push_ab_failed_tip", "Adres defterini sunucuya senkronize edemedi"), + ("synced_peer_readded_tip", "Son oturumlar listesinde bulunan cihazlar adres defterine geri senkronize edilecektir."), + ("Change Color", "Rengi Değiştir"), + ("Primary Color", "Birincil Renk"), + ("HSV Color", "HSV Rengi"), + ("Installation Successful!", "Kurulum Başarılı!"), + ("Installation failed!", "Kurulum başarısız!"), + ("Reverse mouse wheel", "Ters fare tekerleği"), + ("{} sessions", "{} oturum"), + ("scam_title", "Dolandırılıyor Olabilirsiniz!"), + ("scam_text1", "Eğer tanımadığınız ve güvenmediğiniz birisiyle telefonda konuşuyorsanız ve sizden RustDesk'i kullanmanızı ve hizmeti başlatmanızı istiyorsa devam etmeyin ve hemen telefonu kapatın."), + ("scam_text2", "Muhtemelen paranızı veya diğer özel bilgilerinizi çalmaya çalışan dolandırıcılardır."), + ("Don't show again", "Bir daha gösterme"), + ("I Agree", "Kabul Ediyorum"), + ("Decline", "Reddet"), + ("Timeout in minutes", "Zaman aşımı (dakika)"), + ("auto_disconnect_option_tip", "Kullanıcı etkin olmadığında gelen oturumları otomatik olarak kapat"), + ("Connection failed due to inactivity", "Etkin olmama nedeniyle otomatik olarak bağlantı kesildi"), + ("Check for software update on startup", "Başlangıçta yazılım güncellemesini kontrol et"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Lütfen RustDesk Server Pro'yu {} veya daha yeni bir sürüme yükseltin!"), + ("pull_group_failed_tip", "Grup yenilenemedi"), + ("Filter by intersection", "Kesişim noktasına göre filtrele"), + ("Remove wallpaper during incoming sessions", "Gelen oturumlar sırasında duvar kağıdını kaldır"), + ("Test", "Test"), + ("display_is_plugged_out_msg", "Ekran fişi çekilmiş, ilk ekrana geç."), + ("No displays", "Görüntü yok"), + ("Open in new window", "Yeni pencerede aç"), + ("Show displays as individual windows", "Ekranları ayrı pencereler olarak göster"), + ("Use all my displays for the remote session", "Uzak oturum için tüm ekranlarımı kullan"), + ("selinux_tip", "Cihazınızda SELinux etkin olduğundan, RustDesk'in kontrollü tarafta düzgün çalışmasını engelleyebilir."), + ("Change view", "Görünümü değiştir"), + ("Big tiles", "Büyük döşemeler"), + ("Small tiles", "Küçük döşemeler"), + ("List", "Liste"), + ("Virtual display", "Sanal ekran"), + ("Plug out all", "Tümünü çıkar"), + ("True color (4:4:4)", "Gerçek renk (4:4:4)"), + ("Enable blocking user input", "Kullanıcı girişini engellemeyi etkinleştir"), + ("id_input_tip", "Bir ID, doğrudan IP veya portlu bir etki alanı (:) girebilirsiniz.\nBaşka bir sunucudaki bir cihaza erişmek istiyorsanız lütfen sunucu adresini (@?key=) ekleyin, örneğin,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nGenel bir sunucudaki bir cihaza erişmek istiyorsanız lütfen \"@public\" girin, genel sunucu için anahtara gerek yoktur.\n\nİlk bağlantıda bir aktarma bağlantısının kullanılmasını zorlamak istiyorsanız ID'nin sonuna \"/r\" ekleyin, örneğin, \"9123456234/r\"."), + ("privacy_mode_impl_mag_tip", "Mod 1"), + ("privacy_mode_impl_virtual_display_tip", "Mod 2"), + ("Enter privacy mode", "Gizlilik moduna gir"), + ("Exit privacy mode", "Gizlilik modundan çık"), + ("idd_not_support_under_win10_2004_tip", "Dolaylı ekran sürücüsü desteklenmiyor. Windows 10, sürüm 2004 veya daha yenisi gereklidir."), + ("input_source_1_tip", "Giriş kaynağı 1"), + ("input_source_2_tip", "Giriş kaynağı 2"), + ("Swap control-command key", "Kontrol-komut tuşunu değiştir"), + ("swap-left-right-mouse", "Sol-sağ fare tuşlarını değiştir"), + ("2FA code", "2FA kodu"), + ("More", "Daha"), + ("enable-2fa-title", "İki faktörlü kimlik doğrulamayı etkinleştir"), + ("enable-2fa-desc", "Lütfen kimlik doğrulayıcınızı şimdi kurun. Telefonunuzda veya masaüstünüzde Authy, Microsoft veya Google Authenticator gibi bir kimlik doğrulayıcı uygulaması kullanabilirsiniz. İki faktörlü kimlik doğrulamayı etkinleştirmek için QR kodunu uygulamanızla tarayın ve uygulamanızın gösterdiği kodu girin."), + ("wrong-2fa-code", "Kod doğrulanamıyor. Kod ve yerel saat ayarlarının doğru olduğundan emin olun."), + ("enter-2fa-title", "İki faktörlü kimlik doğrulama"), + ("Email verification code must be 6 characters.", "E-posta doğrulama kodu 6 karakterden oluşmalıdır."), + ("2FA code must be 6 digits.", "2FA kodu 6 haneli olmalıdır."), + ("Multiple Windows sessions found", "Birden fazla Windows oturumu bulundu"), + ("Please select the session you want to connect to", "Lütfen bağlanmak istediğiniz oturumu seçin"), + ("powered_by_me", "RustDesk tarafından desteklenmektedir"), + ("outgoing_only_desk_tip", "Bu özelleştirilmiş bir sürümdür.\nDiğer cihazlara bağlanabilirsiniz, ancak diğer cihazlar cihazınıza bağlanamaz."), + ("preset_password_warning", "Bu özelleştirilmiş sürüm, önceden ayarlanmış bir parola ile birlikte gelir. Bu parolayı bilen herkes cihazınızın tam kontrolünü ele geçirebilir. Bunu beklemiyorsanız yazılımı hemen kaldırın."), + ("Security Alert", "Güvenlik Uyarısı"), + ("My address book", "Adres defterim"), + ("Personal", "Kişisel"), + ("Owner", "Sahip"), + ("Set shared password", "Paylaşılan parolayı ayarla"), + ("Exist in", "İçinde varolan"), + ("Read-only", "Salt okunur"), + ("Read/Write", "Okuma/Yazma"), + ("Full Control", "Tam Kontrol"), + ("share_warning_tip", "Yukarıdaki alanlar paylaşılır ve başkaları tarafından görülebilir"), + ("Everyone", "Herkes"), + ("ab_web_console_tip", "Web konsolu hakkında daha fazla bilgi"), + ("allow-only-conn-window-open-tip", "Yalnızca RustDesk penceresi açıksa bağlantıya izin ver"), + ("no_need_privacy_mode_no_physical_displays_tip", "Fiziksel ekran yok, gizlilik modunu kullanmaya gerek yok."), + ("Follow remote cursor", "Uzak imleci takip et"), + ("Follow remote window focus", "Uzak pencere odağını takip et"), + ("default_proxy_tip", "Varsayılan protokol ve port Socks5 ve 1080'dir."), + ("no_audio_input_device_tip", "Ses girişi aygıtı bulunamadı."), + ("Incoming", "Gelen"), + ("Outgoing", "Giden"), + ("Clear Wayland screen selection", "Wayland ekran seçimini temizle"), + ("clear_Wayland_screen_selection_tip", "Ekran seçimini temizledikten sonra paylaşılacak ekranı tekrar seçebilirsiniz."), + ("confirm_clear_Wayland_screen_selection_tip", "Wayland ekran seçimini temizlemek istediğinizden emin misiniz?"), + ("android_new_voice_call_tip", "Yeni bir sesli arama isteği alındı. Kabul ederseniz sesli iletişime geçilecektir."), + ("texture_render_tip", "Resimleri daha pürüzsüz hale getirmek için doku oluşturmayı kullanın. Oluşturma sorunlarıyla karşılaşırsanız bu seçeneği devre dışı bırakmayı deneyebilirsiniz."), + ("Use texture rendering", "Doku oluşturmayı kullan"), + ("Floating window", "Yüzen pencere"), + ("floating_window_tip", "RustDesk arka plan hizmetini açık tutmaya yardımcı olur"), + ("Keep screen on", "Ekranı açık tut"), + ("Never", "Asla"), + ("During controlled", "Kontrol sırasında"), + ("During service is on", "Servis açıkken"), + ("Capture screen using DirectX", "DirectX kullanarak ekran görüntüsü al"), + ("Back", "Geri"), + ("Apps", "Uygulamalar"), + ("Volume up", "Sesi yükselt"), + ("Volume down", "Sesi azalt"), + ("Power", "Güç"), + ("Telegram bot", "Telegram botu"), + ("enable-bot-tip", "Bu özelliği etkinleştirirseniz botunuzdan 2FA kodunu alabilirsiniz. Aynı zamanda bağlantı bildirimi işlevi de görebilir."), + ("enable-bot-desc", "1. @BotFather ile bir sohbet açın.\n2. \"/newbot\" komutunu gönderin. Bu adımı tamamladıktan sonra bir jeton alacaksınız.\n3. Yeni oluşturduğunuz botla bir sohbet başlatın. Etkinleştirmek için eğik çizgiyle (\"/\") başlayan \"/merhaba\" gibi bir mesaj gönderin.\n"), + ("cancel-2fa-confirm-tip", "2FA'yı iptal etmek istediğinizden emin misiniz?"), + ("cancel-bot-confirm-tip", "Telegram botunu iptal etmek istediğinizden emin misiniz?"), + ("About RustDesk", "RustDesk Hakkında"), + ("Send clipboard keystrokes", "Panoya tuş vuruşlarını gönder"), + ("network_error_tip", "Lütfen ağ bağlantınızı kontrol edin ve ardından yeniden dene'ye tıklayın."), + ("Unlock with PIN", "PIN ile kilidi açın"), + ("Requires at least {} characters", "En az {} karakter gerektirir"), + ("Wrong PIN", "Yanlış PIN"), + ("Set PIN", "PIN'i ayarla"), + ("Enable trusted devices", "Güvenilir cihazları etkinleştir"), + ("Manage trusted devices", "Güvenilir cihazları yönet"), + ("Platform", "Platform"), + ("Days remaining", "Kalan gün sayısı"), + ("enable-trusted-devices-tip", "Güvenilir cihazlarda 2FA doğrulamasını atla"), + ("Parent directory", "Üst dizin"), + ("Resume", "Devam ettir"), + ("Invalid file name", "Geçersiz dosya adı"), + ("one-way-file-transfer-tip", "Kontrol edilen tarafta tek yönlü dosya transferi aktiftir."), + ("Authentication Required", "Kimlik Doğrulama Gerekli"), + ("Authenticate", "Kimlik Doğrula"), + ("web_id_input_tip", "Aynı sunucuda bir kimlik girebilirsiniz, web istemcisinde doğrudan IP erişimi desteklenmez.\nBaşka bir sunucudaki bir cihaza erişmek istiyorsanız lütfen sunucu adresini (@?key=) ekleyin, örneğin,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nGenel bir sunucudaki bir cihaza erişmek istiyorsanız, lütfen \"@public\" girin, genel sunucu için anahtara gerek yoktur."), + ("Download", "İndir"), + ("Upload folder", "Klasör yükle"), + ("Upload files", "Dosya yükle"), + ("Clipboard is synchronized", "Pano senkronize edildi"), + ("Update client clipboard", "İstemci panosunu güncelle"), + ("Untagged", "Etiketsiz"), + ("new-version-of-{}-tip", "{}'nin yeni bir sürümü mevcut"), + ("Accessible devices", "Erişilebilir cihazlar"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Lütfen uzak tarafta RustDesk istemcisini {} sürümüne veya daha yenisine güncelleyin!"), + ("d3d_render_tip", "D3D oluşturma etkinleştirildiğinde, bazı bilgisayarlarda uzak kontrol ekranı siyah görünebilir."), + ("Use D3D rendering", "D3D oluşturmayı kullan"), + ("Printer", "Yazıcı"), + ("printer-os-requirement-tip", "Yazıcı çıkış fonksiyonu için Windows 10 veya üzeri gereklidir."), + ("printer-requires-installed-{}-client-tip", "Uzaktan yazdırmayı kullanabilmek için bu cihaza {} yüklenmesi gerekir."), + ("printer-{}-not-installed-tip", "{} Yazıcısı yüklü değil."), + ("printer-{}-ready-tip", "{} Yazıcısı kuruldu ve kullanıma hazır."), + ("Install {} Printer", "{} Yazıcısını Yükle"), + ("Outgoing Print Jobs", "Giden Yazdırma İşleri"), + ("Incoming Print Jobs", "Gelen Yazdırma İşleri"), + ("Incoming Print Job", "Gelen Yazdırma İşi"), + ("use-the-default-printer-tip", "Varsayılan yazıcıyı kullan"), + ("use-the-selected-printer-tip", "Seçili yazıcıyı kullan"), + ("auto-print-tip", "Seçili yazıcıyı kullanarak otomatik olarak yazdır."), + ("print-incoming-job-confirm-tip", "Uzak bir kaynaktan yazdırma işi aldınız. Bunu kendi tarafınızda çalıştırmak ister misiniz?"), + ("remote-printing-disallowed-tile-tip", "Uzak Yazdırma engellendi"), + ("remote-printing-disallowed-text-tip", "Kontrol edilen tarafın izin ayarları Uzak Yazdırmaya izin vermiyor."), + ("save-settings-tip", "Ayarları kaydet"), + ("dont-show-again-tip", "Bunu bir daha gösterme"), + ("Take screenshot", "Ekran görüntüsü al"), + ("Taking screenshot", "Ekran görüntüsü alınıyor"), + ("screenshot-merged-screen-not-supported-tip", "Birden fazla ekranın ekran görüntülerinin birleştirilmesi şu anda desteklenmiyor. Lütfen tek bir ekrana geçin ve tekrar deneyin."), + ("screenshot-action-tip", "Lütfen ekran görüntüsüyle nasıl devam edeceğinizi seçin."), + ("Save as", "Farklı kaydet"), + ("Copy to clipboard", "Panoya kopyala"), + ("Enable remote printer", "Uzak yazıcıyı etkinleştir"), + ("Downloading {}", "{} indiriliyor"), + ("{} Update", "{} Güncellemesi"), + ("{}-to-update-tip", "{} şimdi kapanacak ve yeni sürüm kurulacak."), + ("download-new-version-failed-tip", "İndirme başarısız oldu. Tekrar deneyebilir veya 'İndir' düğmesine tıklayarak sürüm sayfasından manuel olarak indirip güncelleyebilirsiniz."), + ("Auto update", "Otomatik güncelleme"), + ("update-failed-check-msi-tip", "Kurulum yöntemi denetimi başarısız oldu. Sürüm sayfasından indirmek ve manuel olarak yükseltmek için lütfen \"İndir\" düğmesine tıklayın."), + ("websocket_tip", "WebSocket kullanıldığında yalnızca aktarma bağlantıları desteklenir."), + ("Use WebSocket", "WebSocket'ı kullan"), + ("Trackpad speed", "İzleme paneli hızı"), + ("Default trackpad speed", "Varsayılan izleme paneli hızı"), + ("Numeric one-time password", "Sayısal tek seferlik parola"), + ("Enable IPv6 P2P connection", "IPv6 P2P bağlantısını etkinleştir"), + ("Enable UDP hole punching", "UDP delik açmayı etkinleştir"), + ("View camera", "Kamerayı görüntüle"), + ("Enable camera", "Kamerayı etkinleştir"), + ("No cameras", "Kamera yok"), + ("view_camera_unsupported_tip", "Uzak cihaz, kameranın görüntülenmesini desteklemiyor."), + ("Terminal", "Terminal"), + ("Enable terminal", "Terminali etkinleştir"), + ("New tab", "Yeni sekme"), + ("Keep terminal sessions on disconnect", "Bağlantı kesildiğinde terminal oturumlarını açık tut"), + ("Terminal (Run as administrator)", "Terminal (Yönetici olarak çalıştır)"), + ("terminal-admin-login-tip", "Lütfen kontrol edilen tarafın yönetici kullanıcı adı ve parolasını giriniz."), + ("Failed to get user token.", "Kullanıcı belirteci alınamadı."), + ("Incorrect username or password.", "Hatalı kullanıcı adı veya parola."), + ("The user is not an administrator.", "Kullanıcı bir yönetici değil."), + ("Failed to check if the user is an administrator.", "Kullanıcının yönetici olup olmadığı kontrol edilemedi."), + ("Supported only in the installed version.", "Sadece yüklü sürümde desteklenir."), + ("elevation_username_tip", "Kullanıcı adı veya etki alanı\\kullanıcı adı girin"), + ("Preparing for installation ...", "Kuruluma hazırlanıyor..."), + ("Show my cursor", "İmlecimi göster"), + ("Scale custom", "Özel ölçekte"), + ("Custom scale slider", "Özel ölçek kaydırıcısı"), + ("Decrease", "Azalt"), + ("Increase", "Arttır"), + ("Show virtual mouse", "Sanal fareyi göster"), + ("Virtual mouse size", "Sanal fare boyutu"), + ("Small", "Küçük"), + ("Large", "Büyük"), + ("Show virtual joystick", "Sanal joystiği göster"), + ("Edit note", "Notu düzenle"), + ("Alias", "Takma ad"), + ("ScrollEdge", "Kaydırma kenarı"), + ("Allow insecure TLS fallback", "Güvensiz TLS geri dönüşüne izin ver"), + ("allow-insecure-tls-fallback-tip", "Varsayılan olarak, RustDesk sunucu sertifikasını TLS kullanarak protokoller için doğrular.\nBu seçenek etkinleştirildiğinde, doğrulama başarısızlığı durumunda RustDesk doğrulama adımını atlayarak işleme devam eder."), + ("Disable UDP", "UDP'yi devre dışı bırak"), + ("disable-udp-tip", "Yalnızca TCP kullanılıp kullanılmayacağını kontrol eder.\nBu seçenek etkinleştirildiğinde, RustDesk artık UDP 21116'yı kullanmayacak, bunun yerine TCP 21116 kullanılacaktır."), + ("server-oss-not-support-tip", "NOT: RustDesk sunucu OSS'si bu özelliği içermemektedir."), + ("input note here", "Notu buraya girin"), + ("note-at-conn-end-tip", "Bağlantı bittiğinde not sorulsun"), + ("Show terminal extra keys", "Terminal ek tuşlarını göster"), + ("Relative mouse mode", "Fareyi göreli modda kullan"), + ("rel-mouse-not-supported-peer-tip", "Karşı taraf göreli fare modunu desteklemiyor"), + ("rel-mouse-not-ready-tip", "Göreli fare modu henüz hazır değil"), + ("rel-mouse-lock-failed-tip", "Göreli fare kilitlenemedi"), + ("rel-mouse-exit-{}-tip", "Göreli fare modundan çıkmak için {}"), + ("rel-mouse-permission-lost-tip", "Göreli fare izinleri geçerli değil"), + ("Changelog", "Değişiklik Günlüğü"), + ("keep-awake-during-outgoing-sessions-label", "Giden oturumlar süresince ekranı açık tutun"), + ("keep-awake-during-incoming-sessions-label", "Gelen oturumlar süresince ekranı açık tutun"), + ].iter().cloned().collect(); +} From 4ae577c3c4dfe36d5a03169f4ed10aced2dc334e Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Mon, 26 Jan 2026 14:14:35 +0800 Subject: [PATCH 102/277] Revert "Updated tr.rs (#14115)" (#14158) This reverts commit 204e81a700a1436d0b12900b7d9d7a0cd2ef2ce8. --- src/tr.rs | 743 ------------------------------------------------------ 1 file changed, 743 deletions(-) delete mode 100644 src/tr.rs diff --git a/src/tr.rs b/src/tr.rs deleted file mode 100644 index 08f8de37f..000000000 --- a/src/tr.rs +++ /dev/null @@ -1,743 +0,0 @@ -lazy_static::lazy_static! { -pub static ref T: std::collections::HashMap<&'static str, &'static str> = - [ - ("Status", "Durum"), - ("Your Desktop", "Sizin Masaüstünüz"), - ("desk_tip", "Masaüstünüze bu ID ve parola ile erişilebilir"), - ("Password", "Parola"), - ("Ready", "Hazır"), - ("Established", "Bağlantı sağlandı"), - ("connecting_status", "Bağlanılıyor "), - ("Enable service", "Servisi aktif et"), - ("Start service", "Servisi başlat"), - ("Service is running", "Servis çalışıyor"), - ("Service is not running", "Servis çalışmıyor"), - ("not_ready_status", "Hazır değil. Bağlantınızı kontrol edin"), - ("Control Remote Desktop", "Uzak Masaüstünü Denetle"), - ("Transfer file", "Dosya transferi"), - ("Connect", "Bağlan"), - ("Recent sessions", "Son oturumlar"), - ("Address book", "Adres Defteri"), - ("Confirmation", "Onayla"), - ("TCP tunneling", "TCP tünelleri"), - ("Remove", "Kaldır"), - ("Refresh random password", "Yeni rastgele parola oluştur"), - ("Set your own password", "Kendi parolanı oluştur"), - ("Enable keyboard/mouse", "Klavye ve Fareye izin ver"), - ("Enable clipboard", "Kopyalanan geçici veriye izin ver"), - ("Enable file transfer", "Dosya Transferine izin ver"), - ("Enable TCP tunneling", "TCP Tüneline izin ver"), - ("IP Whitelisting", "İzinli IP listesi"), - ("ID/Relay Server", "ID/Relay Sunucusu"), - ("Import server config", "Sunucu ayarlarını içe aktar"), - ("Export Server Config", "Sunucu Yapılandırmasını Dışa Aktar"), - ("Import server configuration successfully", "Sunucu ayarları başarıyla içe aktarıldı"), - ("Export server configuration successfully", "Sunucu yapılandırmasını başarıyla dışa aktar"), - ("Invalid server configuration", "Geçersiz sunucu ayarı"), - ("Clipboard is empty", "Kopyalanan geçici veri boş"), - ("Stop service", "Servisi Durdur"), - ("Change ID", "ID Değiştir"), - ("Your new ID", "Yeni ID'niz"), - ("length %min% to %max%", "uzunluk %min% ila %max%"), - ("starts with a letter", "bir harfle başlar"), - ("allowed characters", "izin verilen karakterler"), - ("id_change_tip", "Yalnızca a-z, A-Z, 0-9, - (dash) ve _ (alt çizgi) karakterlerini kullanabilirsiniz. İlk karakter a-z veya A-Z olmalıdır. Uzunluk 6 ile 16 karakter arasında olmalıdır."), - ("Website", "Website"), - ("About", "Hakkında"), - ("Slogan_tip", "Bu kaotik dünyada gönülden yapıldı!"), - ("Privacy Statement", "Gizlilik Beyanı"), - ("Mute", "Sustur"), - ("Build Date", "Derleme Tarihi"), - ("Version", "Sürüm"), - ("Home", "Ana Sayfa"), - ("Audio Input", "Ses Girişi"), - ("Enhancements", "Geliştirmeler"), - ("Hardware Codec", "Donanımsal Codec"), - ("Adaptive bitrate", "Uyarlanabilir Bit Hızı"), - ("ID Server", "ID Sunucu"), - ("Relay Server", "Relay Sunucu"), - ("API Server", "API Sunucu"), - ("invalid_http", "http:// veya https:// ile başlamalıdır"), - ("Invalid IP", "Geçersiz IP adresi"), - ("Invalid format", "Hatalı Format"), - ("server_not_support", "Henüz sunucu tarafından desteklenmiyor"), - ("Not available", "Erişilebilir değil"), - ("Too frequent", "Çok sık"), - ("Cancel", "İptal"), - ("Skip", "Atla"), - ("Close", "Kapat"), - ("Retry", "Tekrar Dene"), - ("OK", "Tamam"), - ("Password Required", "Parola Gerekli"), - ("Please enter your password", "Lütfen parolanızı giriniz"), - ("Remember password", "Parolayı hatırla"), - ("Wrong Password", "Hatalı parola"), - ("Do you want to enter again?", "Tekrar giriş yapmak ister misiniz?"), - ("Connection Error", "Bağlantı Hatası"), - ("Error", "Hata"), - ("Reset by the peer", "Eş tarafından sıfırlandı"), - ("Connecting...", "Bağlanılıyor..."), - ("Connection in progress. Please wait.", "Bağlantı sağlanıyor. Lütfen bekleyiniz."), - ("Please try 1 minute later", "Lütfen 1 dakika sonra tekrar deneyiniz"), - ("Login Error", "Giriş Hatalı"), - ("Successful", "Başarılı"), - ("Connected, waiting for image...", "Bağlandı. Görüntü bekleniyor..."), - ("Name", "Ad"), - ("Type", "Tip"), - ("Modified", "Değiştirildi"), - ("Size", "Boyut"), - ("Show Hidden Files", "Gizli Dosyaları Göster"), - ("Receive", "Al"), - ("Send", "Gönder"), - ("Refresh File", "Dosyayı yenile"), - ("Local", "Yerel"), - ("Remote", "Uzak"), - ("Remote Computer", "Uzak Bilgisayar"), - ("Local Computer", "Yerel Bilgisayar"), - ("Confirm Delete", "Silmeyi Onayla"), - ("Delete", "Sil"), - ("Properties", "Özellikler"), - ("Multi Select", "Çoklu Seçim"), - ("Select All", "Tümünü Seç"), - ("Unselect All", "Tüm Seçimi Kaldır"), - ("Empty Directory", "Boş Klasör"), - ("Not an empty directory", "Klasör boş değil"), - ("Are you sure you want to delete this file?", "Bu dosyayı silmek istediğinize emin misiniz?"), - ("Are you sure you want to delete this empty directory?", "Bu boş klasörü silmek istediğinize emin misiniz?"), - ("Are you sure you want to delete the file of this directory?", "Bu klasördeki dosyayı silmek istediğinize emin misiniz?"), - ("Do this for all conflicts", "Bunu tüm çakışmalar için yap"), - ("This is irreversible!", "Bu işlem geri döndürülemez!"), - ("Deleting", "Siliniyor"), - ("files", "dosyalar"), - ("Waiting", "Bekleniyor"), - ("Finished", "Tamamlandı"), - ("Speed", "Hız"), - ("Custom Image Quality", "Özel Görüntü Kalitesi"), - ("Privacy mode", "Gizlilik modu"), - ("Block user input", "Kullanıcı girişini engelle"), - ("Unblock user input", "Kullanı girişine izin ver"), - ("Adjust Window", "Pencereyi Ayarla"), - ("Original", "Orjinal"), - ("Shrink", "Küçült"), - ("Stretch", "Uzat"), - ("Scrollbar", "Kaydırma çubuğu"), - ("ScrollAuto", "Otomatik Kaydır"), - ("Good image quality", "İyi görüntü kalitesi"), - ("Balanced", "Dengelenmiş"), - ("Optimize reaction time", "Tepki süresini optimize et"), - ("Custom", "Özel"), - ("Show remote cursor", "Uzaktaki fare imlecini göster"), - ("Show quality monitor", "Kalite monitörünü göster"), - ("Disable clipboard", "Hafızadaki kopyalanmışları engelle"), - ("Lock after session end", "Bağlantıdan sonra kilitle"), - ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del Ekle"), - ("Insert Lock", "Kilit Ekle"), - ("Refresh", "Yenile"), - ("ID does not exist", "ID bulunamadı"), - ("Failed to connect to rendezvous server", "ID oluşturma sunucusuna bağlanılamadı"), - ("Please try later", "Daha sonra tekrar deneyiniz"), - ("Remote desktop is offline", "Uzak masaüstü kapalı"), - ("Key mismatch", "Anahtar uyumlu değil"), - ("Timeout", "Zaman aşımı"), - ("Failed to connect to relay server", "Relay sunucusuna bağlanılamadı"), - ("Failed to connect via rendezvous server", "ID oluşturma sunucusuna bağlanılamadı"), - ("Failed to connect via relay server", "Aktarma sunucusuna bağlanılamadı"), - ("Failed to make direct connection to remote desktop", "Uzak masaüstüne doğrudan bağlantı kurulamadı"), - ("Set Password", "Parola ayarla"), - ("OS Password", "İşletim Sistemi Parolası"), - ("install_tip", "Kullanıcı Hesabı Denetimi nedeniyle, RustDesk bir uzak masaüstü olarak düzgün çalışmayabilir. Bu sorunu önlemek için, RustDesk'i sistem seviyesinde kurmak için aşağıdaki butona tıklayın."), - ("Click to upgrade", "Yükseltmek için tıklayınız"), - ("Configure", "Ayarla"), - ("config_acc", "Masaüstünüzü dışarıdan kontrol etmek için RustDesk'e \"Erişilebilirlik\""), - ("config_screen", "Masaüstünüzü dışarıdan kontrol etmek için RustDesk'e \"Ekran Kaydı\" iznini vermeniz gerekir."), - ("Installing ...", "Yükleniyor ..."), - ("Install", "Yükle"), - ("Installation", "Kurulum"), - ("Installation Path", "Kurulacak olan konum"), - ("Create start menu shortcuts", "Başlangıca kısayol oluştur"), - ("Create desktop icon", "Masaüstüne kısayol oluştur"), - ("agreement_tip", "Kurulumu başlatarak, lisans sözleşmesinin şartlarını kabul etmiş olursunuz."), - ("Accept and Install", "Kabul Et ve Yükle"), - ("End-user license agreement", "Son kullanıcı lisans anlaşması"), - ("Generating ...", "Oluşturuluyor..."), - ("Your installation is lower version.", "Kurulumunuz alt sürümdür."), - ("not_close_tcp_tip", "Tüneli kullanırken bu pencereyi kapatmayın"), - ("Listening ...", "Dinleniyor..."), - ("Remote Host", "Uzak Sunucu"), - ("Remote Port", "Uzak Port"), - ("Action", "Eylem"), - ("Add", "Ekle"), - ("Local Port", "Yerel Port"), - ("Local Address", "Yerel Adres"), - ("Change Local Port", "Yerel Port'u Değiştir"), - ("setup_server_tip", "Daha hızlı bağlantı için kendi sunucunuzu kurun"), - ("Too short, at least 6 characters.", "Çok kısa en az 6 karakter gerekli."), - ("The confirmation is not identical.", "Doğrulama yapılamadı."), - ("Permissions", "İzinler"), - ("Accept", "Kabul Et"), - ("Dismiss", "Reddet"), - ("Disconnect", "Bağlanıyı kes"), - ("Enable file copy and paste", "Dosya kopyalamaya ve yapıştırmaya izin ver"), - ("Connected", "Bağlandı"), - ("Direct and encrypted connection", "Doğrudan ve şifreli bağlantı"), - ("Relayed and encrypted connection", "Aktarmalı ve şifreli bağlantı"), - ("Direct and unencrypted connection", "Doğrudan ve şifrelenmemiş bağlantı"), - ("Relayed and unencrypted connection", "Aktarmalı ve şifrelenmemiş bağlantı"), - ("Enter Remote ID", "Uzak ID'yi Girin"), - ("Enter your password", "Parolanızı girin"), - ("Logging in...", "Giriş yapılıyor..."), - ("Enable RDP session sharing", "RDP oturum paylaşımını etkinleştir"), - ("Auto Login", "Otomatik giriş"), - ("Enable direct IP access", "Doğrudan IP Erişimini Etkinleştir"), - ("Rename", "Yeniden adlandır"), - ("Space", "Boşluk"), - ("Create desktop shortcut", "Masaüstü kısayolu oluşturun"), - ("Change Path", "Yolu değiştir"), - ("Create Folder", "Klasör oluşturun"), - ("Please enter the folder name", "Lütfen klasör adını girin"), - ("Fix it", "Düzenle"), - ("Warning", "Uyarı"), - ("Login screen using Wayland is not supported", "Wayland kullanan giriş ekranı desteklenmiyor"), - ("Reboot required", "Yeniden başlatma gerekli"), - ("Unsupported display server", "Desteklenmeyen görüntü sunucusu"), - ("x11 expected", "x11 bekleniyor"), - ("Port", "Port"), - ("Settings", "Ayarlar"), - ("Username", "Kullanıcı Adı"), - ("Invalid port", "Geçersiz port"), - ("Closed manually by the peer", "Eş tarafından manuel olarak kapatıldı"), - ("Enable remote configuration modification", "Uzaktan yapılandırma değişikliğini etkinleştir"), - ("Run without install", "Yüklemeden çalıştır"), - ("Connect via relay", "Aktarmalı üzerinden bağlan"), - ("Always connect via relay", "Her zaman aktarmalı üzerinden bağlan"), - ("whitelist_tip", "Bu masaüstüne yalnızca yetkili IP adresleri bağlanabilir"), - ("Login", "Giriş yap"), - ("Verify", "Doğrula"), - ("Remember me", "Beni hatırla"), - ("Trust this device", "Bu cihaza güvenin"), - ("Verification code", "Doğrulama kodu"), - ("verification_tip", "doğrulama tipi"), - ("Logout", "Çıkış yap"), - ("Tags", "Etiketler"), - ("Search ID", "ID Arama"), - ("whitelist_sep", "Virgül, noktalı virgül, boşluk veya yeni satır ile ayrılmış"), - ("Add ID", "ID Ekle"), - ("Add Tag", "Etiket Ekle"), - ("Unselect all tags", "Tüm etiketlerin seçimini kaldır"), - ("Network error", "Bağlantı hatası"), - ("Username missed", "Kullanıcı adı boş"), - ("Password missed", "Parola boş"), - ("Wrong credentials", "Yanlış kimlik bilgileri"), - ("The verification code is incorrect or has expired", "Doğrulama kodu hatalı veya süresi dolmuş"), - ("Edit Tag", "Etiketi düzenle"), - ("Forget Password", "Parolayı Unut"), - ("Favorites", "Favoriler"), - ("Add to Favorites", "Favorilere ekle"), - ("Remove from Favorites", "Favorilerden çıkar"), - ("Empty", "Boş"), - ("Invalid folder name", "Geçersiz klasör adı"), - ("Socks5 Proxy", "Socks5 Proxy"), - ("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"), - ("Discovered", "Keşfedilenler"), - ("install_daemon_tip", "Başlangıçta başlamak için sistem hizmetini yüklemeniz gerekir."), - ("Remote ID", "Uzak ID"), - ("Paste", "Yapıştır"), - ("Paste here?", "Buraya yapıştır?"), - ("Are you sure to close the connection?", "Bağlantıyı kapatmak istediğinize emin misiniz?"), - ("Download new version", "Yeni sürümü indir"), - ("Touch mode", "Dokunmatik mod"), - ("Mouse mode", "Fare modu"), - ("One-Finger Tap", "Tek Parmakla Dokunma"), - ("Left Mouse", "Sol Fare"), - ("One-Long Tap", "Tek-Uzun Dokunma"), - ("Two-Finger Tap", "İki-Parmak Dokunma"), - ("Right Mouse", "Sağ Fare"), - ("One-Finger Move", "Tek Parmakla Hareket"), - ("Double Tap & Move", "Çift Dokun ve Taşı"), - ("Mouse Drag", "Fare Sürükleme"), - ("Three-Finger vertically", "Dikey olarak üç parmak"), - ("Mouse Wheel", "Fare Tekerliği"), - ("Two-Finger Move", "İki Parmakla Hareket"), - ("Canvas Move", "Tuval Hareketi"), - ("Pinch to Zoom", "İki parmakla yakınlaştır"), - ("Canvas Zoom", "Tuval Yakınlaştırma"), - ("Reset canvas", "Tuvali sıfırla"), - ("No permission of file transfer", "Dosya aktarımı izni yok"), - ("Note", "Not"), - ("Connection", "Bağlantı"), - ("Share screen", "Ekranı Paylaş"), - ("Chat", "Mesajlaş"), - ("Total", "Toplam"), - ("items", "ögeler"), - ("Selected", "Seçildi"), - ("Screen Capture", "Ekran Görüntüsü"), - ("Input Control", "Giriş Kontrolü"), - ("Audio Capture", "Ses Yakalama"), - ("Do you accept?", "Kabul ediyor musun?"), - ("Open System Setting", "Sistem Ayarını Aç"), - ("How to get Android input permission?", "Android giriş izni nasıl alınır?"), - ("android_input_permission_tip1", "Uzak bir cihazın Android cihazınızı fare veya dokunma yoluyla kontrol edebilmesi için, RustDesk'in \"Erişilebilirlik\" özelliğini kullanmasına izin vermelisiniz."), - ("android_input_permission_tip2", "Sonraki sistem ayarları sayfasına gidin, [Yüklü Hizmetler]'i bulun ve erişin, [RustDesk Girişi] hizmetini etkinleştirin."), - ("android_new_connection_tip", "Yeni bir kontrol talebi alındı, cihazınızı kontrol etmesine izin verilsin mi."), - ("android_service_will_start_tip", "Ekran Yakalamanın etkinleştirilmesi, hizmeti otomatik olarak başlatacak ve diğer cihazların bu cihazdan bağlantı talep etmesine izin verecektir."), - ("android_stop_service_tip", "Hizmetin kapatılması, kurulan tüm bağlantıları otomatik olarak kapatacaktır."), - ("android_version_audio_tip", "Mevcut Android sürümü ses yakalamayı desteklemiyor, lütfen Android 10 veya sonraki bir sürüme yükseltin."), - ("android_start_service_tip", "Ekran paylaşım hizmetini başlatmak için [Hizmeti başlat] ögesine dokunun veya [Ekran Görüntüsü] iznini etkinleştirin."), - ("android_permission_may_not_change_tip", "Kurulan bağlantılara ait izinler, yeniden bağlantı kurulana kadar anında değiştirilemez."), - ("Account", "Hesap"), - ("Overwrite", "Üzerine yaz"), - ("This file exists, skip or overwrite this file?", "Bu dosya var, bu dosya atlansın veya üzerine yazılsın mı?"), - ("Quit", "Çıkış"), - ("Help", "Yardım"), - ("Failed", "Arızalı"), - ("Succeeded", "başarılı"), - ("Someone turns on privacy mode, exit", "Birisi gizlilik modunu açarsa, çık"), - ("Unsupported", "desteklenmiyor"), - ("Peer denied", "eş reddedildi"), - ("Please install plugins", "Lütfen eklentileri yükleyin"), - ("Peer exit", "Eş çıkışı"), - ("Failed to turn off", "Kapatılamadı"), - ("Turned off", "Kapatıldı"), - ("Language", "Dil"), - ("Keep RustDesk background service", "RustDesk arka plan hizmetini sürdürün"), - ("Ignore Battery Optimizations", "Pil Optimizasyonlarını Yoksay"), - ("android_open_battery_optimizations_tip", "Bu özelliği devre dışı bırakmak istiyorsanız lütfen bir sonraki RustDesk uygulama ayarları sayfasına gidin, [Pil] ögesini bulun ve girin, [Sınırsız] ögesinin işaretini kaldırın"), - ("Start on boot", "Önyüklemede başla"), - ("Start the screen sharing service on boot, requires special permissions", "Ekran paylaşım hizmetini önyüklemede başlatmak için özel izinler gerekir"), - ("Connection not allowed", "Bağlantıya izin verilmedi"), - ("Legacy mode", "Eski mod"), - ("Map mode", "Haritalama modu"), - ("Translate mode", "Çeviri modu"), - ("Use permanent password", "Kalıcı parola kullan"), - ("Use both passwords", "İki parolayı da kullan"), - ("Set permanent password", "Kalıcı parola oluştur"), - ("Enable remote restart", "Uzaktan yeniden başlatmayı aktif et"), - ("Restart remote device", "Uzaktaki cihazı yeniden başlat"), - ("Are you sure you want to restart", "Yeniden başlatmak istediğine emin misin?"), - ("Restarting remote device", "Uzaktan yeniden başlatılıyor"), - ("remote_restarting_tip", "Uzak cihaz yeniden başlatılıyor, lütfen bu mesaj kutusunu kapatın ve bir süre sonra kalıcı parola ile yeniden bağlanın"), - ("Copied", "Kopyalandı"), - ("Exit Fullscreen", "Tam Ekrandan Çık"), - ("Fullscreen", "Tam Ekran"), - ("Mobile Actions", "Mobil İşlemler"), - ("Select Monitor", "Monitörü Seç"), - ("Control Actions", "Kontrol Eylemleri"), - ("Display Settings", "Görüntü Ayarları"), - ("Ratio", "Oran"), - ("Image Quality", "Görüntü Kalitesi"), - ("Scroll Style", "Kaydırma Stili"), - ("Show Toolbar", "Araç Çubuğunu Göster"), - ("Hide Toolbar", "Araç Çubuğunu Gizle"), - ("Direct Connection", "Doğrudan Bağlantı"), - ("Relay Connection", "Aktarmalı Bağlantı"), - ("Secure Connection", "Güvenli Bağlantı"), - ("Insecure Connection", "Güvenli Olmayan Bağlantı"), - ("Scale original", "Orijinal ölçekte"), - ("Scale adaptive", "Uyarlanabilir ölçekte"), - ("General", "Genel"), - ("Security", "Güvenlik"), - ("Theme", "Tema"), - ("Dark Theme", "Koyu Tema"), - ("Light Theme", "Açık Tema"), - ("Dark", "Koyu"), - ("Light", "Açık"), - ("Follow System", "Sisteme Uy"), - ("Enable hardware codec", "Donanımsal codec aktif et"), - ("Unlock Security Settings", "Güvenlik Ayarlarını Aç"), - ("Enable audio", "Sesi Aktif Et"), - ("Unlock Network Settings", "Ağ Ayarlarını Aç"), - ("Server", "Sunucu"), - ("Direct IP Access", "Doğrudan IP Erişimi"), - ("Proxy", "Vekil"), - ("Apply", "Uygula"), - ("Disconnect all devices?", "Tüm cihazların bağlantısı kesilsin mi?"), - ("Clear", "Temizle"), - ("Audio Input Device", "Ses Giriş Aygıtı"), - ("Use IP Whitelisting", "IP Beyaz Listeyi Kullan"), - ("Network", "Ağ"), - ("Pin Toolbar", "Araç Çubuğunu Sabitle"), - ("Unpin Toolbar", "Araç Çubuğunu Sabitlemeyi Kaldır"), - ("Recording", "Kaydediliyor"), - ("Directory", "Dizin"), - ("Automatically record incoming sessions", "Gelen oturumları otomatik olarak kaydet"), - ("Automatically record outgoing sessions", "Giden oturumları otomatik olarak kaydet"), - ("Change", "Değiştir"), - ("Start session recording", "Oturum kaydını başlat"), - ("Stop session recording", "Oturum kaydını sonlandır"), - ("Enable recording session", "Kayıt Oturumunu Aktif Et"), - ("Enable LAN discovery", "Yerel Ağ Keşfine İzin Ver"), - ("Deny LAN discovery", "Yerel Ağ Keşfine İzin Verme"), - ("Write a message", "Bir mesaj yazın"), - ("Prompt", "İstem"), - ("Please wait for confirmation of UAC...", "UAC onayı için lütfen bekleyiniz..."), - ("elevated_foreground_window_tip", "elevated_foreground_window_tip"), - ("Disconnected", "Bağlantı Kesildi"), - ("Other", "Diğer"), - ("Confirm before closing multiple tabs", "Çoklu sekmeleri kapatmadan önce onayla"), - ("Keyboard Settings", "Klavye Ayarları"), - ("Full Access", "Tam Erişim"), - ("Screen Share", "Ekran Paylaşımı"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland, Ubuntu 21.04 veya daha yüksek bir sürüm gerektirir."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland, linux dağıtımının daha yüksek bir sürümünü gerektirir. Lütfen X11 masaüstünü deneyin veya işletim sisteminizi değiştirin."), - ("JumpLink", "View"), - ("Please Select the screen to be shared(Operate on the peer side).", "Lütfen paylaşılacak ekranı seçiniz (Ekran tarafında çalıştırın)."), - ("Show RustDesk", "RustDesk'i Göster"), - ("This PC", "Bu PC"), - ("or", "veya"), - ("Continue with", "Bununla devam et"), - ("Elevate", "Yükseltme"), - ("Zoom cursor", "Yakınlaştırma imleci"), - ("Accept sessions via password", "Oturumları parola ile kabul etme"), - ("Accept sessions via click", "Tıklama yoluyla oturumları kabul edin"), - ("Accept sessions via both", "Her ikisi aracılığıyla oturumları kabul edin"), - ("Please wait for the remote side to accept your session request...", "Lütfen uzak tarafın oturum isteğinizi kabul etmesini bekleyin..."), - ("One-time Password", "Tek Kullanımlık Parola"), - ("Use one-time password", "Tek seferlik parola kullanın"), - ("One-time password length", "Tek seferlik parola uzunluğu"), - ("Request access to your device", "Cihazınıza erişim talep edin"), - ("Hide connection management window", "Bağlantı yönetimi penceresini gizle"), - ("hide_cm_tip", "Oturumları yalnızca parola ile kabul edebilir ve kalıcı parola kullanıyorsanız gizlemeye izin verin"), - ("wayland_experiment_tip", "Wayland desteği deneysel aşamada olduğundan, gerektiğinde X11'i kullanmanız önerilir"), - ("Right click to select tabs", "Sekmeleri seçmek için sağ tıklayın"), - ("Skipped", "Atlandı"), - ("Add to address book", "Adres Defterine Ekle"), - ("Group", "Grup"), - ("Search", "Ara"), - ("Closed manually by web console", "Web konsoluyla manuel olarak kapatıldı"), - ("Local keyboard type", "Yerel klavye türü"), - ("Select local keyboard type", "Yerel klavye türünü seçin"), - ("software_render_tip", "Linux altında Nvidia grafik kartı kullanıyorsanız ve uzak pencere bağlandıktan hemen sonra kapanıyorsa, açık kaynaklı Nouveau sürücüsüne geçmeyi ve yazılım renderleme seçeneğini seçmeyi deneyin. Yazılımı yeniden başlatmanız gerekebilir."), - ("Always use software rendering", "Her zaman yazılım renderleme kullan"), - ("config_input", "Uzaktaki masaüstünü klavye ile kontrol etmek için RustDesk'e \"Giriş İzleme\" izinleri vermelisiniz."), - ("config_microphone", "Uzaktan konuşmak için RustDesk'e \"Ses Kaydı\" izinleri vermelisiniz."), - ("request_elevation_tip", "Ayrıca, uzak tarafta biri varsa yükseltme isteğinde bulunabilirsiniz."), - ("Wait", "Bekle"), - ("Elevation Error", "Yükseltme Hatası"), - ("Ask the remote user for authentication", "Uzaktaki kullanıcıdan kimlik doğrulamasını isteyin"), - ("Choose this if the remote account is administrator", "Uzak hesap yönetici ise bunu seçin"), - ("Transmit the username and password of administrator", "Yönetici kullanıcı adı ve parolasını iletim yapın"), - ("still_click_uac_tip", "Uzaktaki kullanıcının çalışan RustDesk'in UAC penceresinde hala Tamam'ı tıklaması gerekmektedir."), - ("Request Elevation", "Yükseltme İsteği"), - ("wait_accept_uac_tip", "Lütfen uzaktaki kullanıcının UAC iletişim kutusunu kabul etmesini bekleyin."), - ("Elevate successfully", "Başarıyla yükseltildi"), - ("uppercase", "büyük harf"), - ("lowercase", "küçük harf"), - ("digit", "rakam"), - ("special character", "özel karakter"), - ("length>=8", "uzunluk>=8"), - ("Weak", "Zayıf"), - ("Medium", "Orta"), - ("Strong", "Güçlü"), - ("Switch Sides", "Tarafları Değiştir"), - ("Please confirm if you want to share your desktop?", "Masaüstünüzü paylaşmak isteyip istemediğinizi onaylayın?"), - ("Display", "Görüntüle"), - ("Default View Style", "Varsayılan Görünüm Stili"), - ("Default Scroll Style", "Varsayılan Kaydırma Stili"), - ("Default Image Quality", "Varsayılan Görüntü Kalitesi"), - ("Default Codec", "Varsayılan Kodlayıcı"), - ("Bitrate", "Bit Hızı"), - ("FPS", "FPS"), - ("Auto", "Otomatik"), - ("Other Default Options", "Diğer Varsayılan Seçenekler"), - ("Voice call", "Sesli görüşme"), - ("Text chat", "Metin sohbeti"), - ("Stop voice call", "Sesli görüşmeyi durdur"), - ("relay_hint_tip", "Doğrudan bağlanmak mümkün olmayabilir; aktarmalı bağlanmayı deneyebilirsiniz. Ayrıca, ilk denemenizde aktarma sunucusu kullanmak istiyorsanız ID'nin sonuna \"/r\" ekleyebilir veya son oturum kartındaki \"Her Zaman Aktarmalı Üzerinden Bağlan\" seçeneğini seçebilirsiniz."), - ("Reconnect", "Yeniden Bağlan"), - ("Codec", "Kodlayıcı"), - ("Resolution", "Çözünürlük"), - ("No transfers in progress", "Devam eden aktarımlar yok"), - ("Set one-time password length", "Bir seferlik parola uzunluğunu ayarla"), - ("RDP Settings", "RDP Ayarları"), - ("Sort by", "Sırala"), - ("New Connection", "Yeni Bağlantı"), - ("Restore", "Geri Yükle"), - ("Minimize", "Simge Durumuna Küçült"), - ("Maximize", "Büyüt"), - ("Your Device", "Cihazınız"), - ("empty_recent_tip", "Üzgünüz, henüz son oturum yok!\nYeni bir plan yapma zamanı."), - ("empty_favorite_tip", "Henüz favori cihazınız yok mu?\nBağlanacak ve favorilere eklemek için birini bulalım!"), - ("empty_lan_tip", "Hayır, henüz hiçbir cihaz bulamadık gibi görünüyor."), - ("empty_address_book_tip", "Üzgünüm, şu anda adres defterinizde kayıtlı cihaz yok gibi görünüyor."), - ("Empty Username", "Boş Kullanıcı Adı"), - ("Empty Password", "Boş Parola"), - ("Me", "Ben"), - ("identical_file_tip", "Bu dosya, cihazın dosyası ile aynıdır."), - ("show_monitors_tip", "Monitörleri araç çubuğunda göster"), - ("View Mode", "Görünüm Modu"), - ("login_linux_tip", "X masaüstü oturumu başlatmak için uzaktaki Linux hesabına giriş yapmanız gerekiyor"), - ("verify_rustdesk_password_tip", "RustDesk parolasını doğrulayın"), - ("remember_account_tip", "Bu hesabı hatırla"), - ("os_account_desk_tip", "Bu hesap, uzaktaki işletim sistemine giriş yapmak ve başsız masaüstü oturumunu etkinleştirmek için kullanılır."), - ("OS Account", "İşletim Sistemi Hesabı"), - ("another_user_login_title_tip", "Başka bir kullanıcı zaten oturum açtı"), - ("another_user_login_text_tip", "Bağlantıyı Kapat"), - ("xorg_not_found_title_tip", "Xorg bulunamadı"), - ("xorg_not_found_text_tip", "Lütfen Xorg'u yükleyin"), - ("no_desktop_title_tip", "Masaüstü mevcut değil"), - ("no_desktop_text_tip", "Lütfen GNOME masaüstünü yükleyin"), - ("No need to elevate", "Yükseltmeye gerek yok"), - ("System Sound", "Sistem Sesi"), - ("Default", "Varsayılan"), - ("New RDP", "Yeni RDP"), - ("Fingerprint", "Parmak İzi"), - ("Copy Fingerprint", "Parmak İzini Kopyala"), - ("no fingerprints", "parmak izi yok"), - ("Select a peer", "Bir cihaz seçin"), - ("Select peers", "Cihazları seçin"), - ("Plugins", "Eklentiler"), - ("Uninstall", "Kaldır"), - ("Update", "Güncelle"), - ("Enable", "Etkinleştir"), - ("Disable", "Devre Dışı Bırak"), - ("Options", "Seçenekler"), - ("resolution_original_tip", "Orijinal çözünürlük"), - ("resolution_fit_local_tip", "Yerel çözünürlüğe sığdır"), - ("resolution_custom_tip", "Özel çözünürlük"), - ("Collapse toolbar", "Araç çubuğunu daralt"), - ("Accept and Elevate", "Kabul Et ve Yükselt"), - ("accept_and_elevate_btn_tooltip", "Bağlantıyı kabul et ve UAC izinlerini yükselt."), - ("clipboard_wait_response_timeout_tip", "Kopyalama yanıtı için zaman aşımına uğradı."), - ("Incoming connection", "Gelen bağlantı"), - ("Outgoing connection", "Giden bağlantı"), - ("Exit", "Çıkış"), - ("Open", "Aç"), - ("logout_tip", "Çıkış yapmak istediğinizden emin misiniz?"), - ("Service", "Hizmet"), - ("Start", "Başlat"), - ("Stop", "Durdur"), - ("exceed_max_devices", "Yönetilen cihazların maksimum sayısına ulaştınız."), - ("Sync with recent sessions", "Son oturumlarla senkronize et"), - ("Sort tags", "Etiketleri sırala"), - ("Open connection in new tab", "Bağlantıyı yeni sekmede aç"), - ("Move tab to new window", "Sekmeyi yeni pencereye taşı"), - ("Can not be empty", "Boş olamaz"), - ("Already exists", "Zaten var"), - ("Change Password", "Parolayı Değiştir"), - ("Refresh Password", "Parolayı Yenile"), - ("ID", "Kimlik"), - ("Grid View", "Izgara Görünümü"), - ("List View", "Liste Görünümü"), - ("Select", "Seç"), - ("Toggle Tags", "Etiketleri Değiştir"), - ("pull_ab_failed_tip", "Adres defterini yenileyemedi"), - ("push_ab_failed_tip", "Adres defterini sunucuya senkronize edemedi"), - ("synced_peer_readded_tip", "Son oturumlar listesinde bulunan cihazlar adres defterine geri senkronize edilecektir."), - ("Change Color", "Rengi Değiştir"), - ("Primary Color", "Birincil Renk"), - ("HSV Color", "HSV Rengi"), - ("Installation Successful!", "Kurulum Başarılı!"), - ("Installation failed!", "Kurulum başarısız!"), - ("Reverse mouse wheel", "Ters fare tekerleği"), - ("{} sessions", "{} oturum"), - ("scam_title", "Dolandırılıyor Olabilirsiniz!"), - ("scam_text1", "Eğer tanımadığınız ve güvenmediğiniz birisiyle telefonda konuşuyorsanız ve sizden RustDesk'i kullanmanızı ve hizmeti başlatmanızı istiyorsa devam etmeyin ve hemen telefonu kapatın."), - ("scam_text2", "Muhtemelen paranızı veya diğer özel bilgilerinizi çalmaya çalışan dolandırıcılardır."), - ("Don't show again", "Bir daha gösterme"), - ("I Agree", "Kabul Ediyorum"), - ("Decline", "Reddet"), - ("Timeout in minutes", "Zaman aşımı (dakika)"), - ("auto_disconnect_option_tip", "Kullanıcı etkin olmadığında gelen oturumları otomatik olarak kapat"), - ("Connection failed due to inactivity", "Etkin olmama nedeniyle otomatik olarak bağlantı kesildi"), - ("Check for software update on startup", "Başlangıçta yazılım güncellemesini kontrol et"), - ("upgrade_rustdesk_server_pro_to_{}_tip", "Lütfen RustDesk Server Pro'yu {} veya daha yeni bir sürüme yükseltin!"), - ("pull_group_failed_tip", "Grup yenilenemedi"), - ("Filter by intersection", "Kesişim noktasına göre filtrele"), - ("Remove wallpaper during incoming sessions", "Gelen oturumlar sırasında duvar kağıdını kaldır"), - ("Test", "Test"), - ("display_is_plugged_out_msg", "Ekran fişi çekilmiş, ilk ekrana geç."), - ("No displays", "Görüntü yok"), - ("Open in new window", "Yeni pencerede aç"), - ("Show displays as individual windows", "Ekranları ayrı pencereler olarak göster"), - ("Use all my displays for the remote session", "Uzak oturum için tüm ekranlarımı kullan"), - ("selinux_tip", "Cihazınızda SELinux etkin olduğundan, RustDesk'in kontrollü tarafta düzgün çalışmasını engelleyebilir."), - ("Change view", "Görünümü değiştir"), - ("Big tiles", "Büyük döşemeler"), - ("Small tiles", "Küçük döşemeler"), - ("List", "Liste"), - ("Virtual display", "Sanal ekran"), - ("Plug out all", "Tümünü çıkar"), - ("True color (4:4:4)", "Gerçek renk (4:4:4)"), - ("Enable blocking user input", "Kullanıcı girişini engellemeyi etkinleştir"), - ("id_input_tip", "Bir ID, doğrudan IP veya portlu bir etki alanı (:) girebilirsiniz.\nBaşka bir sunucudaki bir cihaza erişmek istiyorsanız lütfen sunucu adresini (@?key=) ekleyin, örneğin,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nGenel bir sunucudaki bir cihaza erişmek istiyorsanız lütfen \"@public\" girin, genel sunucu için anahtara gerek yoktur.\n\nİlk bağlantıda bir aktarma bağlantısının kullanılmasını zorlamak istiyorsanız ID'nin sonuna \"/r\" ekleyin, örneğin, \"9123456234/r\"."), - ("privacy_mode_impl_mag_tip", "Mod 1"), - ("privacy_mode_impl_virtual_display_tip", "Mod 2"), - ("Enter privacy mode", "Gizlilik moduna gir"), - ("Exit privacy mode", "Gizlilik modundan çık"), - ("idd_not_support_under_win10_2004_tip", "Dolaylı ekran sürücüsü desteklenmiyor. Windows 10, sürüm 2004 veya daha yenisi gereklidir."), - ("input_source_1_tip", "Giriş kaynağı 1"), - ("input_source_2_tip", "Giriş kaynağı 2"), - ("Swap control-command key", "Kontrol-komut tuşunu değiştir"), - ("swap-left-right-mouse", "Sol-sağ fare tuşlarını değiştir"), - ("2FA code", "2FA kodu"), - ("More", "Daha"), - ("enable-2fa-title", "İki faktörlü kimlik doğrulamayı etkinleştir"), - ("enable-2fa-desc", "Lütfen kimlik doğrulayıcınızı şimdi kurun. Telefonunuzda veya masaüstünüzde Authy, Microsoft veya Google Authenticator gibi bir kimlik doğrulayıcı uygulaması kullanabilirsiniz. İki faktörlü kimlik doğrulamayı etkinleştirmek için QR kodunu uygulamanızla tarayın ve uygulamanızın gösterdiği kodu girin."), - ("wrong-2fa-code", "Kod doğrulanamıyor. Kod ve yerel saat ayarlarının doğru olduğundan emin olun."), - ("enter-2fa-title", "İki faktörlü kimlik doğrulama"), - ("Email verification code must be 6 characters.", "E-posta doğrulama kodu 6 karakterden oluşmalıdır."), - ("2FA code must be 6 digits.", "2FA kodu 6 haneli olmalıdır."), - ("Multiple Windows sessions found", "Birden fazla Windows oturumu bulundu"), - ("Please select the session you want to connect to", "Lütfen bağlanmak istediğiniz oturumu seçin"), - ("powered_by_me", "RustDesk tarafından desteklenmektedir"), - ("outgoing_only_desk_tip", "Bu özelleştirilmiş bir sürümdür.\nDiğer cihazlara bağlanabilirsiniz, ancak diğer cihazlar cihazınıza bağlanamaz."), - ("preset_password_warning", "Bu özelleştirilmiş sürüm, önceden ayarlanmış bir parola ile birlikte gelir. Bu parolayı bilen herkes cihazınızın tam kontrolünü ele geçirebilir. Bunu beklemiyorsanız yazılımı hemen kaldırın."), - ("Security Alert", "Güvenlik Uyarısı"), - ("My address book", "Adres defterim"), - ("Personal", "Kişisel"), - ("Owner", "Sahip"), - ("Set shared password", "Paylaşılan parolayı ayarla"), - ("Exist in", "İçinde varolan"), - ("Read-only", "Salt okunur"), - ("Read/Write", "Okuma/Yazma"), - ("Full Control", "Tam Kontrol"), - ("share_warning_tip", "Yukarıdaki alanlar paylaşılır ve başkaları tarafından görülebilir"), - ("Everyone", "Herkes"), - ("ab_web_console_tip", "Web konsolu hakkında daha fazla bilgi"), - ("allow-only-conn-window-open-tip", "Yalnızca RustDesk penceresi açıksa bağlantıya izin ver"), - ("no_need_privacy_mode_no_physical_displays_tip", "Fiziksel ekran yok, gizlilik modunu kullanmaya gerek yok."), - ("Follow remote cursor", "Uzak imleci takip et"), - ("Follow remote window focus", "Uzak pencere odağını takip et"), - ("default_proxy_tip", "Varsayılan protokol ve port Socks5 ve 1080'dir."), - ("no_audio_input_device_tip", "Ses girişi aygıtı bulunamadı."), - ("Incoming", "Gelen"), - ("Outgoing", "Giden"), - ("Clear Wayland screen selection", "Wayland ekran seçimini temizle"), - ("clear_Wayland_screen_selection_tip", "Ekran seçimini temizledikten sonra paylaşılacak ekranı tekrar seçebilirsiniz."), - ("confirm_clear_Wayland_screen_selection_tip", "Wayland ekran seçimini temizlemek istediğinizden emin misiniz?"), - ("android_new_voice_call_tip", "Yeni bir sesli arama isteği alındı. Kabul ederseniz sesli iletişime geçilecektir."), - ("texture_render_tip", "Resimleri daha pürüzsüz hale getirmek için doku oluşturmayı kullanın. Oluşturma sorunlarıyla karşılaşırsanız bu seçeneği devre dışı bırakmayı deneyebilirsiniz."), - ("Use texture rendering", "Doku oluşturmayı kullan"), - ("Floating window", "Yüzen pencere"), - ("floating_window_tip", "RustDesk arka plan hizmetini açık tutmaya yardımcı olur"), - ("Keep screen on", "Ekranı açık tut"), - ("Never", "Asla"), - ("During controlled", "Kontrol sırasında"), - ("During service is on", "Servis açıkken"), - ("Capture screen using DirectX", "DirectX kullanarak ekran görüntüsü al"), - ("Back", "Geri"), - ("Apps", "Uygulamalar"), - ("Volume up", "Sesi yükselt"), - ("Volume down", "Sesi azalt"), - ("Power", "Güç"), - ("Telegram bot", "Telegram botu"), - ("enable-bot-tip", "Bu özelliği etkinleştirirseniz botunuzdan 2FA kodunu alabilirsiniz. Aynı zamanda bağlantı bildirimi işlevi de görebilir."), - ("enable-bot-desc", "1. @BotFather ile bir sohbet açın.\n2. \"/newbot\" komutunu gönderin. Bu adımı tamamladıktan sonra bir jeton alacaksınız.\n3. Yeni oluşturduğunuz botla bir sohbet başlatın. Etkinleştirmek için eğik çizgiyle (\"/\") başlayan \"/merhaba\" gibi bir mesaj gönderin.\n"), - ("cancel-2fa-confirm-tip", "2FA'yı iptal etmek istediğinizden emin misiniz?"), - ("cancel-bot-confirm-tip", "Telegram botunu iptal etmek istediğinizden emin misiniz?"), - ("About RustDesk", "RustDesk Hakkında"), - ("Send clipboard keystrokes", "Panoya tuş vuruşlarını gönder"), - ("network_error_tip", "Lütfen ağ bağlantınızı kontrol edin ve ardından yeniden dene'ye tıklayın."), - ("Unlock with PIN", "PIN ile kilidi açın"), - ("Requires at least {} characters", "En az {} karakter gerektirir"), - ("Wrong PIN", "Yanlış PIN"), - ("Set PIN", "PIN'i ayarla"), - ("Enable trusted devices", "Güvenilir cihazları etkinleştir"), - ("Manage trusted devices", "Güvenilir cihazları yönet"), - ("Platform", "Platform"), - ("Days remaining", "Kalan gün sayısı"), - ("enable-trusted-devices-tip", "Güvenilir cihazlarda 2FA doğrulamasını atla"), - ("Parent directory", "Üst dizin"), - ("Resume", "Devam ettir"), - ("Invalid file name", "Geçersiz dosya adı"), - ("one-way-file-transfer-tip", "Kontrol edilen tarafta tek yönlü dosya transferi aktiftir."), - ("Authentication Required", "Kimlik Doğrulama Gerekli"), - ("Authenticate", "Kimlik Doğrula"), - ("web_id_input_tip", "Aynı sunucuda bir kimlik girebilirsiniz, web istemcisinde doğrudan IP erişimi desteklenmez.\nBaşka bir sunucudaki bir cihaza erişmek istiyorsanız lütfen sunucu adresini (@?key=) ekleyin, örneğin,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nGenel bir sunucudaki bir cihaza erişmek istiyorsanız, lütfen \"@public\" girin, genel sunucu için anahtara gerek yoktur."), - ("Download", "İndir"), - ("Upload folder", "Klasör yükle"), - ("Upload files", "Dosya yükle"), - ("Clipboard is synchronized", "Pano senkronize edildi"), - ("Update client clipboard", "İstemci panosunu güncelle"), - ("Untagged", "Etiketsiz"), - ("new-version-of-{}-tip", "{}'nin yeni bir sürümü mevcut"), - ("Accessible devices", "Erişilebilir cihazlar"), - ("upgrade_remote_rustdesk_client_to_{}_tip", "Lütfen uzak tarafta RustDesk istemcisini {} sürümüne veya daha yenisine güncelleyin!"), - ("d3d_render_tip", "D3D oluşturma etkinleştirildiğinde, bazı bilgisayarlarda uzak kontrol ekranı siyah görünebilir."), - ("Use D3D rendering", "D3D oluşturmayı kullan"), - ("Printer", "Yazıcı"), - ("printer-os-requirement-tip", "Yazıcı çıkış fonksiyonu için Windows 10 veya üzeri gereklidir."), - ("printer-requires-installed-{}-client-tip", "Uzaktan yazdırmayı kullanabilmek için bu cihaza {} yüklenmesi gerekir."), - ("printer-{}-not-installed-tip", "{} Yazıcısı yüklü değil."), - ("printer-{}-ready-tip", "{} Yazıcısı kuruldu ve kullanıma hazır."), - ("Install {} Printer", "{} Yazıcısını Yükle"), - ("Outgoing Print Jobs", "Giden Yazdırma İşleri"), - ("Incoming Print Jobs", "Gelen Yazdırma İşleri"), - ("Incoming Print Job", "Gelen Yazdırma İşi"), - ("use-the-default-printer-tip", "Varsayılan yazıcıyı kullan"), - ("use-the-selected-printer-tip", "Seçili yazıcıyı kullan"), - ("auto-print-tip", "Seçili yazıcıyı kullanarak otomatik olarak yazdır."), - ("print-incoming-job-confirm-tip", "Uzak bir kaynaktan yazdırma işi aldınız. Bunu kendi tarafınızda çalıştırmak ister misiniz?"), - ("remote-printing-disallowed-tile-tip", "Uzak Yazdırma engellendi"), - ("remote-printing-disallowed-text-tip", "Kontrol edilen tarafın izin ayarları Uzak Yazdırmaya izin vermiyor."), - ("save-settings-tip", "Ayarları kaydet"), - ("dont-show-again-tip", "Bunu bir daha gösterme"), - ("Take screenshot", "Ekran görüntüsü al"), - ("Taking screenshot", "Ekran görüntüsü alınıyor"), - ("screenshot-merged-screen-not-supported-tip", "Birden fazla ekranın ekran görüntülerinin birleştirilmesi şu anda desteklenmiyor. Lütfen tek bir ekrana geçin ve tekrar deneyin."), - ("screenshot-action-tip", "Lütfen ekran görüntüsüyle nasıl devam edeceğinizi seçin."), - ("Save as", "Farklı kaydet"), - ("Copy to clipboard", "Panoya kopyala"), - ("Enable remote printer", "Uzak yazıcıyı etkinleştir"), - ("Downloading {}", "{} indiriliyor"), - ("{} Update", "{} Güncellemesi"), - ("{}-to-update-tip", "{} şimdi kapanacak ve yeni sürüm kurulacak."), - ("download-new-version-failed-tip", "İndirme başarısız oldu. Tekrar deneyebilir veya 'İndir' düğmesine tıklayarak sürüm sayfasından manuel olarak indirip güncelleyebilirsiniz."), - ("Auto update", "Otomatik güncelleme"), - ("update-failed-check-msi-tip", "Kurulum yöntemi denetimi başarısız oldu. Sürüm sayfasından indirmek ve manuel olarak yükseltmek için lütfen \"İndir\" düğmesine tıklayın."), - ("websocket_tip", "WebSocket kullanıldığında yalnızca aktarma bağlantıları desteklenir."), - ("Use WebSocket", "WebSocket'ı kullan"), - ("Trackpad speed", "İzleme paneli hızı"), - ("Default trackpad speed", "Varsayılan izleme paneli hızı"), - ("Numeric one-time password", "Sayısal tek seferlik parola"), - ("Enable IPv6 P2P connection", "IPv6 P2P bağlantısını etkinleştir"), - ("Enable UDP hole punching", "UDP delik açmayı etkinleştir"), - ("View camera", "Kamerayı görüntüle"), - ("Enable camera", "Kamerayı etkinleştir"), - ("No cameras", "Kamera yok"), - ("view_camera_unsupported_tip", "Uzak cihaz, kameranın görüntülenmesini desteklemiyor."), - ("Terminal", "Terminal"), - ("Enable terminal", "Terminali etkinleştir"), - ("New tab", "Yeni sekme"), - ("Keep terminal sessions on disconnect", "Bağlantı kesildiğinde terminal oturumlarını açık tut"), - ("Terminal (Run as administrator)", "Terminal (Yönetici olarak çalıştır)"), - ("terminal-admin-login-tip", "Lütfen kontrol edilen tarafın yönetici kullanıcı adı ve parolasını giriniz."), - ("Failed to get user token.", "Kullanıcı belirteci alınamadı."), - ("Incorrect username or password.", "Hatalı kullanıcı adı veya parola."), - ("The user is not an administrator.", "Kullanıcı bir yönetici değil."), - ("Failed to check if the user is an administrator.", "Kullanıcının yönetici olup olmadığı kontrol edilemedi."), - ("Supported only in the installed version.", "Sadece yüklü sürümde desteklenir."), - ("elevation_username_tip", "Kullanıcı adı veya etki alanı\\kullanıcı adı girin"), - ("Preparing for installation ...", "Kuruluma hazırlanıyor..."), - ("Show my cursor", "İmlecimi göster"), - ("Scale custom", "Özel ölçekte"), - ("Custom scale slider", "Özel ölçek kaydırıcısı"), - ("Decrease", "Azalt"), - ("Increase", "Arttır"), - ("Show virtual mouse", "Sanal fareyi göster"), - ("Virtual mouse size", "Sanal fare boyutu"), - ("Small", "Küçük"), - ("Large", "Büyük"), - ("Show virtual joystick", "Sanal joystiği göster"), - ("Edit note", "Notu düzenle"), - ("Alias", "Takma ad"), - ("ScrollEdge", "Kaydırma kenarı"), - ("Allow insecure TLS fallback", "Güvensiz TLS geri dönüşüne izin ver"), - ("allow-insecure-tls-fallback-tip", "Varsayılan olarak, RustDesk sunucu sertifikasını TLS kullanarak protokoller için doğrular.\nBu seçenek etkinleştirildiğinde, doğrulama başarısızlığı durumunda RustDesk doğrulama adımını atlayarak işleme devam eder."), - ("Disable UDP", "UDP'yi devre dışı bırak"), - ("disable-udp-tip", "Yalnızca TCP kullanılıp kullanılmayacağını kontrol eder.\nBu seçenek etkinleştirildiğinde, RustDesk artık UDP 21116'yı kullanmayacak, bunun yerine TCP 21116 kullanılacaktır."), - ("server-oss-not-support-tip", "NOT: RustDesk sunucu OSS'si bu özelliği içermemektedir."), - ("input note here", "Notu buraya girin"), - ("note-at-conn-end-tip", "Bağlantı bittiğinde not sorulsun"), - ("Show terminal extra keys", "Terminal ek tuşlarını göster"), - ("Relative mouse mode", "Fareyi göreli modda kullan"), - ("rel-mouse-not-supported-peer-tip", "Karşı taraf göreli fare modunu desteklemiyor"), - ("rel-mouse-not-ready-tip", "Göreli fare modu henüz hazır değil"), - ("rel-mouse-lock-failed-tip", "Göreli fare kilitlenemedi"), - ("rel-mouse-exit-{}-tip", "Göreli fare modundan çıkmak için {}"), - ("rel-mouse-permission-lost-tip", "Göreli fare izinleri geçerli değil"), - ("Changelog", "Değişiklik Günlüğü"), - ("keep-awake-during-outgoing-sessions-label", "Giden oturumlar süresince ekranı açık tutun"), - ("keep-awake-during-incoming-sessions-label", "Gelen oturumlar süresince ekranı açık tutun"), - ].iter().cloned().collect(); -} From b0c8e65c6efef5d301ef087a4e8148821b0fad57 Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Mon, 26 Jan 2026 07:15:45 +0100 Subject: [PATCH 103/277] Italian language update (#14129) --- src/lang/it.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 5bb4f2349..f83232a0f 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -737,7 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", "Premi {} per uscire."), ("rel-mouse-permission-lost-tip", "È stata revocato l'accesso alla tastiera. La modalità mouse relativa è stata disabilitata."), ("Changelog", "Novità programma"), - ("keep-awake-during-outgoing-sessions-label", ""), - ("keep-awake-during-incoming-sessions-label", ""), + ("keep-awake-during-outgoing-sessions-label", "Mantieni lo schermo attivo durante le sessioni in uscita"), + ("keep-awake-during-incoming-sessions-label", "Mantieni lo schermo attivo durante le sessioni in ingresso"), ].iter().cloned().collect(); } From 226d7417b2f6c1e864f0ea26d4cad4a828f850c6 Mon Sep 17 00:00:00 2001 From: Hugo Breda <11139838+agarre@users.noreply.github.com> Date: Mon, 26 Jan 2026 03:15:58 -0300 Subject: [PATCH 104/277] PT-BR language update (#14135) * PT-BR language update @rustdesk Please merge. Thanks * Update ptbr.rs * Update ptbr.rs Please submit, i will get back soon and finish all other stuff. --- src/lang/ptbr.rs | 102 +++++++++++++++++++++++------------------------ 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index ed8f2a4ba..e26d6b2c8 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -672,72 +672,72 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote-printing-disallowed-text-tip", "As configurações do dispositivo controlado não permitem impressão remota."), ("save-settings-tip", "Salvar configurações"), ("dont-show-again-tip", "Não mostrar novamente"), - ("Take screenshot", ""), - ("Taking screenshot", ""), + ("Take screenshot", "Capturar de tela"), + ("Taking screenshot", "Capturando tela"), ("screenshot-merged-screen-not-supported-tip", ""), ("screenshot-action-tip", ""), - ("Save as", ""), - ("Copy to clipboard", ""), - ("Enable remote printer", ""), + ("Save as", "Salvar como"), + ("Copy to clipboard", "Copiar para área de transferência"), + ("Enable remote printer", "Habilitar impressora remota"), ("Downloading {}", ""), ("{} Update", ""), ("{}-to-update-tip", ""), - ("download-new-version-failed-tip", ""), - ("Auto update", ""), - ("update-failed-check-msi-tip", ""), - ("websocket_tip", ""), - ("Use WebSocket", ""), - ("Trackpad speed", ""), + ("download-new-version-failed-tip", "Falha no download. Você pode tentar novamente ou clicar no botão \"Download\" para baixar da página releases e atualizar manualmente."), + ("Auto update", "Atualização automática"), + ("update-failed-check-msi-tip", "Falha na verificação do método de instalação. Clique no botão \"Download\" para baixar da página releases e atualizar manualmente."), + ("websocket_tip", "Usando WebSocket, apenas conexões via relay são suportadas."), + ("Use WebSocket", "Usar WebSocket"), + ("Trackpad speed", "Velocidade do trackpad"), ("Default trackpad speed", ""), - ("Numeric one-time password", ""), - ("Enable IPv6 P2P connection", ""), - ("Enable UDP hole punching", ""), + ("Numeric one-time password", "Senha numérica de uso único"), + ("Enable IPv6 P2P connection", "Habilitar conexão IPv6 P2P"), + ("Enable UDP hole punching", "Habilitar UDP hole punching"), ("View camera", "Visualizar câmera"), ("Enable camera", "Ativar câmera"), ("No cameras", "Sem câmeras"), ("view_camera_unsupported_tip", "O dispositivo remoto não suporta visualização da câmera."), - ("Terminal", ""), - ("Enable terminal", ""), - ("New tab", ""), - ("Keep terminal sessions on disconnect", ""), - ("Terminal (Run as administrator)", ""), - ("terminal-admin-login-tip", ""), - ("Failed to get user token.", ""), - ("Incorrect username or password.", ""), - ("The user is not an administrator.", ""), - ("Failed to check if the user is an administrator.", ""), - ("Supported only in the installed version.", ""), - ("elevation_username_tip", ""), - ("Preparing for installation ...", ""), - ("Show my cursor", ""), + ("Terminal", "Terminal"), + ("Enable terminal", "Habilitar Terminal"), + ("New tab", "Nova aba"), + ("Keep terminal sessions on disconnect", "Manter sessões de terminal ao desconectar"), + ("Terminal (Run as administrator)", "Terminal (Executar como administrador)"), + ("terminal-admin-login-tip", "Insira o nome do usuário e senha de administrador do dispositivo controlado."), + ("Failed to get user token.", "Falha ao obter token do usuário."), + ("Incorrect username or password.", "Usuário ou senha incorretos"), + ("The user is not an administrator.", "O usuário não é administrador"), + ("Failed to check if the user is an administrator.", "Falha ao verificar se o usuário é administrador"), + ("Supported only in the installed version.", "Funciona somente na versão instalada"), + ("elevation_username_tip", "Insira o nome do usuário ou domínio\\usuário"), + ("Preparing for installation ...", "Preparando para instalação ..."), + ("Show my cursor", "Mostrar meu cursor"), ("Scale custom", "Escala personalizada"), ("Custom scale slider", "Controle deslizante de escala personalizada"), ("Decrease", "Diminuir"), ("Increase", "Aumentar"), - ("Show virtual mouse", ""), - ("Virtual mouse size", ""), - ("Small", ""), - ("Large", ""), + ("Show virtual mouse", "Mostrar mouse virtual"), + ("Virtual mouse size", "Tamanho do mouse virtual"), + ("Small", "Pequeno"), + ("Large", "Grande"), ("Show virtual joystick", ""), - ("Edit note", ""), - ("Alias", ""), - ("ScrollEdge", ""), + ("Edit note", "Editar nota"), + ("Alias", "Apelido"), + ("ScrollEdge", "Rolagem nas bordas"), ("Allow insecure TLS fallback", ""), - ("allow-insecure-tls-fallback-tip", ""), - ("Disable UDP", ""), - ("disable-udp-tip", ""), - ("server-oss-not-support-tip", ""), - ("input note here", ""), - ("note-at-conn-end-tip", ""), - ("Show terminal extra keys", ""), - ("Relative mouse mode", ""), - ("rel-mouse-not-supported-peer-tip", ""), - ("rel-mouse-not-ready-tip", ""), - ("rel-mouse-lock-failed-tip", ""), - ("rel-mouse-exit-{}-tip", ""), - ("rel-mouse-permission-lost-tip", ""), - ("Changelog", ""), - ("keep-awake-during-outgoing-sessions-label", ""), - ("keep-awake-during-incoming-sessions-label", ""), + ("allow-insecure-tls-fallback-tip", "Por padrão, o RustDesk verifica o certificado do servidor para protocolos que usam TLS.\nCom esta opção habilitada, o RustDesk ignorará a verificação e prosseguirá em caso de falha."), + ("Disable UDP", "Desabilitar UDP"), + ("disable-udp-tip", "Controla se deve usar somente TCP.\nCom esta opção habilitada, o RustDesk não usará mais UDP 21116, TCP 21116 será usado no lugar."), + ("server-oss-not-support-tip", "NOTA: O servidor RustDesk OSS não inclui este recurso."), + ("input note here", "Insira uma nota aqui"), + ("note-at-conn-end-tip", "Solicitar nota ao final da conexão"), + ("Show terminal extra keys", "Mostrar teclas extras do terminal"), + ("Relative mouse mode", "Modo de Mouse Relativo"), + ("rel-mouse-not-supported-peer-tip", "O Modo de Mouse Relativo não é suportado pelo parceiro conectado."), + ("rel-mouse-not-ready-tip", "O Modo de Mouse Relativo ainda não está pronto. Por favor, tente novamente."), + ("rel-mouse-lock-failed-tip", "Falha ao bloquear o cursor. O Modo de Mouse Relativo foi desabilitado."), + ("rel-mouse-exit-{}-tip", "Pressione {} para sair."), + ("rel-mouse-permission-lost-tip", "Permissão de teclado revogada. O Modo Mouse Relativo foi desabilitado."), + ("Changelog", "Registro de alterações"), + ("keep-awake-during-outgoing-sessions-label", "Manter tela ativa durante sessões de saída"), + ("keep-awake-during-incoming-sessions-label", "Manter tela ativa durante sessões de entrada"), ].iter().cloned().collect(); } From f05f2178e59a8e5d74e4822d50beca54f7aec0bb Mon Sep 17 00:00:00 2001 From: Alex Rijckaert Date: Mon, 26 Jan 2026 07:16:21 +0100 Subject: [PATCH 105/277] Update Dutch translations (#14136) --- src/lang/nl.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/nl.rs b/src/lang/nl.rs index c5627abfd..34e35615f 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -737,7 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", "Druk op {} om af te sluiten."), ("rel-mouse-permission-lost-tip", "De toetsenbordcontrole is uitgeschakeld. De relatieve muismodus is uitgeschakeld."), ("Changelog", "Wijzigingenlogboek"), - ("keep-awake-during-outgoing-sessions-label", ""), - ("keep-awake-during-incoming-sessions-label", ""), + ("keep-awake-during-outgoing-sessions-label", "Houd het scherm open tijdens de uitgaande sessies."), + ("keep-awake-during-incoming-sessions-label", "Houd het scherm open tijdens de inkomende sessies."), ].iter().cloned().collect(); } From c76d10a438f64fac521749690d520c5260e23b1b Mon Sep 17 00:00:00 2001 From: Bin Li <47075710+bin-haw@users.noreply.github.com> Date: Tue, 27 Jan 2026 16:38:37 +0800 Subject: [PATCH 106/277] feat(macos): initial privacy mode support [a simple try] (#14102) * feat(macos): add privacy mode support for macOS ## Summary Add privacy mode functionality for macOS platform, allowing remote desktop sessions to hide the screen content from local users. ## Changes ### Core Implementation (src/platform/macos.mm) - Implement screen blackout using CGDisplayGammaTable API - Implement input blocking using CGEventTap to intercept keyboard/mouse - Store and restore original gamma values for proper cleanup ### Privacy Mode Integration (src/privacy_mode.rs, src/privacy_mode/macos.rs) - Add macOS privacy mode implementation with PrivacyMode trait - Register macOS privacy mode in PRIVACY_MODE_CREATOR - Set DEFAULT_PRIVACY_MODE_IMPL for macOS platform - Implement get_supported_privacy_mode_impl() for macOS ### Connection Handling (src/server/connection.rs) - Add supported_privacy_mode_impl to platform_additions for macOS - Enable privacy mode toggle in client UI when connecting via LAN IP ### Localization (src/lang/*.rs) - Add "privacy_mode_impl_macos_tip" translation for en/cn/tw ## Safety & Security - Implements Drop trait to ensure cleanup on normal exit - macOS system automatically restores gamma table on process termination - CGEventTap is automatically released when process terminates - Tested with SIGKILL to verify crash recovery ## Testing - Verified privacy mode toggle works via both ID and LAN IP connection - Verified screen recovery after process crash (kill -9) - Verified input restoration after process termination * refactor: use existing 'Privacy mode' translation key * refactor: rename gamma channel variables for better readability - rename r/g/b to red/green/blue to avoid variable shadowing confusion * fix: add error handling for gamma table restoration with fallback to system reset * fix: add error handling for CGEventTapCreate failure in privacy mode * fix: only set display to black if original gamma was saved successfully * fix: add error handling for CGSetDisplayTransferByTable when setting display to black * fix: improve event tap callback to properly distinguish remote input from local input * fix: missing macos.rs * Fix: Add display validation before restoring gamma values * Fix: Add mutex lock for thread safety in MacSetPrivacyMode * Fix: Handle return values and add missing mouse events in macos privacy mode * fix: only set conn_id after privacy mode is successfully turned on * fix: reimplement privacy mode with stable display identification Address code review concern: original gamma values stored with DisplayID as key could become stale if display list changes between privacy mode activations (e.g., display reconnected with different ID). Solution: - Use UUID instead of DisplayID as storage key (stable across reconnections) - Clear g_originalGammas when privacy mode is turned off - Register CGDisplayReconfigurationCallback to handle hot-plug events - Validate display state via FindDisplayIdByUUID() before restoration Key features: - UUID-based display identification (stable across reconnections) - Hot-plug support via CGDisplayReconfigurationCallback - EventTap auto re-enable on system timeout - Fallback to CGDisplayRestoreColorSyncSettings() for recovery - Detailed error logging with display name/ID/UUID * fix: ensure EventTap runs on main thread and improve gamma restore error handling - Add SetupEventTapOnMainThread() to create EventTap on main thread using dispatch_sync, avoiding potential issues when called from background threads - Add TeardownEventTapOnMainThread() for consistent cleanup on main thread - Check [NSThread isMainThread] to avoid deadlock when already on main thread - Add error tracking for gamma restoration during cleanup - Use CGDisplayRestoreColorSyncSettings() as fallback when individual gamma restoration fails * fix: remove invalid eventMask bits that caused undefined behavior in input blocking * fix: address code review comments for macos privacy mode implementation Changes to src/privacy_mode/macos.rs: - Add check_on_conn_id() in turn_on_privacy() to prevent duplicate activation - Add check_off_conn_id() in turn_off_privacy() to validate connection ID - Add self.conn_id = 0 in clear() to reset connection state Changes to src/platform/macos.mm: - Add link comment for ENIGO_INPUT_EXTRA_VALUE referencing libs/enigo/src/macos/macos_impl.rs - Fix NSLog format string mismatch (5 placeholders vs 4 values) - Make ApplyBlackoutToDisplay() return bool for proper error handling - Return false when UUID is empty since privacy mode requires ALL displays - Add else branches with logging for: - CGGetDisplayTransferByTable failures - Zero gamma table capacity (not supported) - Zero blackout capacity - Remove unused g_uuidToDisplayId variable (was only written, never read) * fix(macos): add early return with privacy mode exit on display hotplug failures Why large-scale changes are needed: The code review suggested adding early return when errors occur in DisplayReconfigurationCallback. However, simply returning early is not enough - when a newly connected display cannot be blacked out, we must exit privacy mode entirely to maintain security guarantees. The challenge is that DisplayReconfigurationCallback already holds g_privacyModeMutex, so calling MacSetPrivacyMode(false) directly would cause a deadlock. This necessitated: 1. Extract TurnOffPrivacyModeInternal() - a lock-free internal function that can be safely called from within the callback 2. Refactor MacSetPrivacyMode(false) branch to use this internal function 3. Add early returns with TurnOffPrivacyModeInternal() calls at each failure point in DisplayReconfigurationCallback Changes in DisplayReconfigurationCallback: - UUID empty: log + exit privacy mode + early return - Gamma table capacity zero: log + exit privacy mode + early return - CGGetDisplayTransferByTable fails: log + exit privacy mode + early return - ApplyBlackoutToDisplay fails: log + exit privacy mode + early return * fix(macos): address code review feedback and improve privacy mode stability Code Review Fixes: - Add detailed comments for potential deadlock scenarios in dispatch_sync with g_privacyModeMutex (SetupEventTapOnMainThread/TeardownEventTapOnMainThread) - Use async dispatch for privacy mode shutdown from DisplayReconfigurationCallback to avoid unregistering callback from within itself - Extract RestoreAllGammas() helper function to reduce code duplication - Fix Drop implementation in macos.rs to call self.clear() for consistency - Add comment explaining why _state parameter is ignored on macOS - Define DISPLAY_RECONFIG_MONITOR_DURATION_MS and GAMMA_CHECK_INTERVAL_MS constants - Add gamma restoration when UUID retrieval fails during privacy mode activation Privacy Mode Stability Improvements (Continuous Resolution Changes): - Implement continuous gamma value monitoring with timer polling after display reconfiguration to handle rapid successive resolution changes - Monitor gamma values every 200ms for 5 seconds after each resolution change - Automatically reapply blackout if system (ColorSync) restores gamma - Add IsDisplayBlackedOut() to detect if display gamma has been restored - Use timestamp-based debouncing: monitoring period automatically extends when new reconfig events occur during active monitoring - Ensure blackout remains effective even under continuous resolution changes where macOS may asynchronously restore gamma values multiple times This ensures privacy mode remains stable and effective when users rapidly change display resolution multiple times in succession. --------- Co-authored-by: libin --- src/platform/macos.mm | 639 ++++++++++++++++++++++++++++++++++++++ src/privacy_mode.rs | 29 +- src/privacy_mode/macos.rs | 81 +++++ src/server/connection.rs | 9 +- 4 files changed, 754 insertions(+), 4 deletions(-) create mode 100644 src/privacy_mode/macos.rs diff --git a/src/platform/macos.mm b/src/platform/macos.mm index 92ee5170b..a9270455b 100644 --- a/src/platform/macos.mm +++ b/src/platform/macos.mm @@ -4,6 +4,13 @@ #include #include +#include +#include +#include +#include +#include +#include + extern "C" bool CanUseNewApiForScreenCaptureCheck() { #ifdef NO_InputMonitoringAuthStatus return false; @@ -292,3 +299,635 @@ extern "C" bool MacSetMode(CGDirectDisplayID display, uint32_t width, uint32_t h CFRelease(allModes); return ret; } + +static CFMachPortRef g_eventTap = NULL; +static CFRunLoopSourceRef g_runLoopSource = NULL; +static std::mutex g_privacyModeMutex; +static bool g_privacyModeActive = false; + +// Flag to request asynchronous shutdown of privacy mode. +// This is set by DisplayReconfigurationCallback when an error occurs, instead of calling +// TurnOffPrivacyModeInternal() directly from within the callback. This avoids potential +// issues with unregistering a callback from within itself, which is not explicitly +// guaranteed to be safe by Apple documentation. +static bool g_privacyModeShutdownRequested = false; + +// Timestamp of the last display reconfiguration event (in milliseconds). +// Used for debouncing rapid successive changes (e.g., multiple resolution changes). +static uint64_t g_lastReconfigTimestamp = 0; + +// Flag indicating whether a delayed blackout reapplication is already scheduled. +// Prevents multiple concurrent delayed tasks from being created. +static bool g_blackoutReapplicationScheduled = false; + +// Use CFStringRef (UUID) as key instead of CGDirectDisplayID for stability across reconnections +// CGDirectDisplayID can change when displays are reconnected, but UUID remains stable +static std::map> g_originalGammas; + +// The event source user data value used by enigo library for injected events. +// This allows us to distinguish remote input (which should be allowed) from local physical input. +// See: libs/enigo/src/macos/macos_impl.rs - ENIGO_INPUT_EXTRA_VALUE +static const int64_t ENIGO_INPUT_EXTRA_VALUE = 100; + +// Duration in milliseconds to monitor and enforce blackout after display reconfiguration. +// macOS may restore default gamma (via ColorSync) at unpredictable times after display changes, +// so we need to actively monitor and reapply blackout during this period. +static const int64_t DISPLAY_RECONFIG_MONITOR_DURATION_MS = 5000; + +// Interval in milliseconds between gamma checks during the monitoring period. +static const int64_t GAMMA_CHECK_INTERVAL_MS = 200; + +// Helper function to get UUID string from DisplayID +static std::string GetDisplayUUID(CGDirectDisplayID displayId) { + CFUUIDRef uuid = CGDisplayCreateUUIDFromDisplayID(displayId); + if (uuid == NULL) { + return ""; + } + CFStringRef uuidStr = CFUUIDCreateString(kCFAllocatorDefault, uuid); + CFRelease(uuid); + if (uuidStr == NULL) { + return ""; + } + char buffer[128]; + if (CFStringGetCString(uuidStr, buffer, sizeof(buffer), kCFStringEncodingUTF8)) { + CFRelease(uuidStr); + return std::string(buffer); + } + CFRelease(uuidStr); + return ""; +} + +// Helper function to get display name from DisplayID +static std::string GetDisplayName(CGDirectDisplayID displayId) { + NSArray *screens = [NSScreen screens]; + for (NSScreen *screen in screens) { + NSDictionary *deviceDescription = [screen deviceDescription]; + NSNumber *screenNumber = [deviceDescription objectForKey:@"NSScreenNumber"]; + CGDirectDisplayID screenDisplayID = [screenNumber unsignedIntValue]; + if (screenDisplayID == displayId) { + // localizedName is available on macOS 10.15+ + if (@available(macOS 10.15, *)) { + NSString *name = [screen localizedName]; + if (name) { + return std::string([name UTF8String]); + } + } + break; + } + } + return "Unknown"; +} + +// Helper function to find DisplayID by UUID from current online displays +static CGDirectDisplayID FindDisplayIdByUUID(const std::string& targetUuid) { + uint32_t count = 0; + CGGetOnlineDisplayList(0, NULL, &count); + if (count == 0) return kCGNullDirectDisplay; + + std::vector displays(count); + CGGetOnlineDisplayList(count, displays.data(), &count); + + for (uint32_t i = 0; i < count; i++) { + std::string uuid = GetDisplayUUID(displays[i]); + if (uuid == targetUuid) { + return displays[i]; + } + } + return kCGNullDirectDisplay; +} + +// Helper function to restore gamma values for all displays in g_originalGammas. +// Returns true if all displays were restored successfully, false if any failed. +// Note: This function does NOT clear g_originalGammas - caller should do that if needed. +static bool RestoreAllGammas() { + bool allSuccess = true; + for (auto const& [uuid, gamma] : g_originalGammas) { + CGDirectDisplayID d = FindDisplayIdByUUID(uuid); + if (d == kCGNullDirectDisplay) { + NSLog(@"Display with UUID %s no longer online, skipping gamma restore", uuid.c_str()); + continue; + } + + uint32_t sampleCount = gamma.size() / 3; + if (sampleCount > 0) { + const CGGammaValue* red = gamma.data(); + const CGGammaValue* green = red + sampleCount; + const CGGammaValue* blue = green + sampleCount; + CGError error = CGSetDisplayTransferByTable(d, sampleCount, red, green, blue); + if (error != kCGErrorSuccess) { + std::string displayName = GetDisplayName(d); + NSLog(@"Failed to restore gamma for display (Name: %s, ID: %u, UUID: %s, error: %d)", + displayName.c_str(), (unsigned)d, uuid.c_str(), error); + allSuccess = false; + } + } + } + return allSuccess; +} + +// Helper function to apply blackout to a single display +static bool ApplyBlackoutToDisplay(CGDirectDisplayID display) { + uint32_t capacity = CGDisplayGammaTableCapacity(display); + if (capacity > 0) { + std::vector zeros(capacity, 0.0f); + CGError error = CGSetDisplayTransferByTable(display, capacity, zeros.data(), zeros.data(), zeros.data()); + if (error != kCGErrorSuccess) { + NSLog(@"ApplyBlackoutToDisplay: Failed to set gamma for display %u (error %d)", (unsigned)display, error); + return false; + } + return true; + } + NSLog(@"ApplyBlackoutToDisplay: Display %u has zero gamma table capacity, blackout not supported", (unsigned)display); + return false; +} + +// Forward declaration - defined later in the file +// Must be called while holding g_privacyModeMutex +static bool TurnOffPrivacyModeInternal(); + +// Helper function to schedule asynchronous shutdown of privacy mode. +// This is called from DisplayReconfigurationCallback when an error occurs, +// instead of calling TurnOffPrivacyModeInternal() directly. This avoids +// potential issues with unregistering a callback from within itself. +// Note: This function should be called while holding g_privacyModeMutex. +static void ScheduleAsyncPrivacyModeShutdown(const char* reason) { + if (g_privacyModeShutdownRequested) { + // Already requested, no need to schedule again + return; + } + g_privacyModeShutdownRequested = true; + NSLog(@"Privacy mode shutdown requested: %s", reason); + + // Schedule the actual shutdown on the main queue asynchronously + // This ensures we're outside the callback when we unregister it + dispatch_async(dispatch_get_main_queue(), ^{ + std::lock_guard lock(g_privacyModeMutex); + if (g_privacyModeShutdownRequested && g_privacyModeActive) { + NSLog(@"Executing deferred privacy mode shutdown"); + TurnOffPrivacyModeInternal(); + } + g_privacyModeShutdownRequested = false; + }); +} + +// Helper function to apply blackout to all online displays. +// Must be called while holding g_privacyModeMutex. +static void ApplyBlackoutToAllDisplays() { + uint32_t onlineCount = 0; + CGGetOnlineDisplayList(0, NULL, &onlineCount); + std::vector onlineDisplays(onlineCount); + CGGetOnlineDisplayList(onlineCount, onlineDisplays.data(), &onlineCount); + + for (uint32_t i = 0; i < onlineCount; i++) { + ApplyBlackoutToDisplay(onlineDisplays[i]); + } +} + +// Helper function to get current timestamp in milliseconds +static uint64_t GetCurrentTimestampMs() { + return (uint64_t)(CFAbsoluteTimeGetCurrent() * 1000.0); +} + +// Helper function to check if a display's gamma is currently blacked out (all zeros). +// Returns true if gamma appears to be blacked out, false otherwise. +static bool IsDisplayBlackedOut(CGDirectDisplayID display) { + uint32_t capacity = CGDisplayGammaTableCapacity(display); + if (capacity == 0) { + return true; // Can't check, assume it's fine + } + + std::vector red(capacity), green(capacity), blue(capacity); + uint32_t sampleCount = 0; + if (CGGetDisplayTransferByTable(display, capacity, red.data(), green.data(), blue.data(), &sampleCount) != kCGErrorSuccess) { + return true; // Can't read, assume it's fine + } + + // Check if all values are zero (or very close to zero) + for (uint32_t i = 0; i < sampleCount; i++) { + if (red[i] > 0.01f || green[i] > 0.01f || blue[i] > 0.01f) { + return false; // Not blacked out + } + } + return true; +} + +// Internal function that monitors and enforces blackout for a period after display reconfiguration. +// This function checks gamma values periodically and reapplies blackout if needed. +// Must NOT be called while holding g_privacyModeMutex (it acquires the lock internally). +static void RunBlackoutMonitor() { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(GAMMA_CHECK_INTERVAL_MS * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{ + std::lock_guard lock(g_privacyModeMutex); + + if (!g_privacyModeActive) { + g_blackoutReapplicationScheduled = false; + return; + } + + uint64_t now = GetCurrentTimestampMs(); + + // Calculate effective end time based on the last reconfig event + uint64_t effectiveEndTime = g_lastReconfigTimestamp + DISPLAY_RECONFIG_MONITOR_DURATION_MS; + + // Check all displays and reapply blackout if any has been restored + uint32_t onlineCount = 0; + CGGetOnlineDisplayList(0, NULL, &onlineCount); + std::vector onlineDisplays(onlineCount); + CGGetOnlineDisplayList(onlineCount, onlineDisplays.data(), &onlineCount); + + bool needsReapply = false; + for (uint32_t i = 0; i < onlineCount; i++) { + if (!IsDisplayBlackedOut(onlineDisplays[i])) { + needsReapply = true; + break; + } + } + + if (needsReapply) { + NSLog(@"Gamma was restored by system, reapplying blackout"); + ApplyBlackoutToAllDisplays(); + } + + // Continue monitoring if we haven't reached the end time + if (now < effectiveEndTime) { + RunBlackoutMonitor(); + } else { + NSLog(@"Blackout monitoring period ended"); + g_blackoutReapplicationScheduled = false; + } + }); +} + +// Helper function to start monitoring and enforcing blackout after display reconfiguration. +// This is used after display reconfiguration events because macOS may restore +// default gamma (via ColorSync) at unpredictable times after display changes. +// Note: This function should be called while holding g_privacyModeMutex. +static void ScheduleDelayedBlackoutReapplication(const char* reason) { + // Update timestamp to current time + g_lastReconfigTimestamp = GetCurrentTimestampMs(); + + NSLog(@"Starting blackout monitor: %s", reason); + + // Only schedule if not already scheduled + if (!g_blackoutReapplicationScheduled) { + g_blackoutReapplicationScheduled = true; + RunBlackoutMonitor(); + } + // If already scheduled, the running monitor will see the updated timestamp + // and extend its monitoring period +} + +// Display reconfiguration callback to handle display connect/disconnect events +// +// IMPORTANT: When errors occur in this callback, we use ScheduleAsyncPrivacyModeShutdown() +// instead of calling TurnOffPrivacyModeInternal() directly. This is because: +// 1. TurnOffPrivacyModeInternal() calls CGDisplayRemoveReconfigurationCallback to unregister +// this callback, and unregistering a callback from within itself is not explicitly +// guaranteed to be safe by Apple documentation. +// 2. Using async dispatch ensures we're completely outside the callback context when +// performing the cleanup, avoiding any potential undefined behavior. +static void DisplayReconfigurationCallback(CGDirectDisplayID display, CGDisplayChangeSummaryFlags flags, void *userInfo) { + (void)userInfo; + + // Note: We need to handle the callback carefully because: + // 1. macOS may call this callback multiple times during display reconfiguration + // 2. The system may restore ColorSync settings after our gamma change + // 3. We should not hold the lock for too long in the callback + + // Skip begin configuration flag - wait for the actual change + if (flags & kCGDisplayBeginConfigurationFlag) { + return; + } + + std::lock_guard lock(g_privacyModeMutex); + + if (!g_privacyModeActive) { + return; + } + + if (flags & kCGDisplayAddFlag) { + // A display was added - apply blackout to it + NSLog(@"Display %u added during privacy mode, applying blackout", (unsigned)display); + std::string uuid = GetDisplayUUID(display); + if (uuid.empty()) { + NSLog(@"Failed to get UUID for newly added display %u, exiting privacy mode", (unsigned)display); + ScheduleAsyncPrivacyModeShutdown("Failed to get UUID for newly added display"); + return; + } + + // Save original gamma if not already saved for this UUID + if (g_originalGammas.find(uuid) == g_originalGammas.end()) { + uint32_t capacity = CGDisplayGammaTableCapacity(display); + if (capacity > 0) { + std::vector red(capacity), green(capacity), blue(capacity); + uint32_t sampleCount = 0; + if (CGGetDisplayTransferByTable(display, capacity, red.data(), green.data(), blue.data(), &sampleCount) == kCGErrorSuccess) { + std::vector all; + all.insert(all.end(), red.begin(), red.begin() + sampleCount); + all.insert(all.end(), green.begin(), green.begin() + sampleCount); + all.insert(all.end(), blue.begin(), blue.begin() + sampleCount); + g_originalGammas[uuid] = all; + } else { + NSLog(@"DisplayReconfigurationCallback: Failed to get gamma table for display %u (UUID: %s), exiting privacy mode", (unsigned)display, uuid.c_str()); + ScheduleAsyncPrivacyModeShutdown("Failed to get gamma table for newly added display"); + return; + } + } else { + NSLog(@"DisplayReconfigurationCallback: Display %u (UUID: %s) has zero gamma table capacity, exiting privacy mode", (unsigned)display, uuid.c_str()); + ScheduleAsyncPrivacyModeShutdown("Newly added display has zero gamma table capacity"); + return; + } + } + + // Apply blackout to the new display immediately + if (!ApplyBlackoutToDisplay(display)) { + NSLog(@"DisplayReconfigurationCallback: Failed to blackout display %u (UUID: %s), exiting privacy mode", (unsigned)display, uuid.c_str()); + ScheduleAsyncPrivacyModeShutdown("Failed to blackout newly added display"); + return; + } + + // Schedule a delayed re-application to handle ColorSync restoration + // macOS may restore default gamma for ALL displays after a new display is added, + // so we need to reapply blackout to all online displays, not just the new one + ScheduleDelayedBlackoutReapplication("after new display added"); + } else if (flags & kCGDisplayRemoveFlag) { + // A display was removed - update our mapping and reapply blackout to remaining displays + NSLog(@"Display %u removed during privacy mode", (unsigned)display); + std::string uuid = GetDisplayUUID(display); + (void)uuid; // UUID retrieved for potential future use or logging + + // When a display is removed, macOS may reconfigure other displays and restore their gamma. + // Schedule a delayed re-application of blackout to all remaining online displays. + ScheduleDelayedBlackoutReapplication("after display removal"); + } else if (flags & kCGDisplaySetModeFlag) { + // Display mode changed (resolution change, ColorSync/Night Shift interference, etc.) + // macOS resets gamma to default when display mode changes, so we need to reapply blackout. + // Schedule a delayed re-application because ColorSync restoration happens asynchronously. + NSLog(@"Display %u mode changed during privacy mode, reapplying blackout", (unsigned)display); + ScheduleDelayedBlackoutReapplication("after display mode change"); + } +} + +CGEventRef MyEventTapCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) { + (void)proxy; + (void)refcon; + + // Handle EventTap being disabled by system timeout + if (type == kCGEventTapDisabledByTimeout) { + NSLog(@"EventTap was disabled by timeout, re-enabling"); + if (g_eventTap) { + CGEventTapEnable(g_eventTap, true); + } + return event; + } + + // Handle EventTap being disabled by user input + if (type == kCGEventTapDisabledByUserInput) { + NSLog(@"EventTap was disabled by user input, re-enabling"); + if (g_eventTap) { + CGEventTapEnable(g_eventTap, true); + } + return event; + } + + // Allow events explicitly injected by enigo (remote input), identified via custom user data. + int64_t userData = CGEventGetIntegerValueField(event, kCGEventSourceUserData); + if (userData == ENIGO_INPUT_EXTRA_VALUE) { + return event; + } + // Block local physical HID input. + if (CGEventGetIntegerValueField(event, kCGEventSourceStateID) == kCGEventSourceStateHIDSystemState) { + return NULL; + } + return event; +} + +// Helper function to set up EventTap on the main thread +// Returns true if EventTap was successfully created and enabled +static bool SetupEventTapOnMainThread() { + __block bool success = false; + + void (^setupBlock)(void) = ^{ + if (g_eventTap) { + // Already set up + success = true; + return; + } + + // Note: kCGEventTapDisabledByTimeout and kCGEventTapDisabledByUserInput are special + // notification types (0xFFFFFFFE and 0xFFFFFFFF) that are delivered via the callback's + // type parameter, not through the event mask. They should NOT be included in eventMask + // as bit-shifting by these values causes undefined behavior. + CGEventMask eventMask = (1 << kCGEventKeyDown) | (1 << kCGEventKeyUp) | + (1 << kCGEventLeftMouseDown) | (1 << kCGEventLeftMouseUp) | + (1 << kCGEventRightMouseDown) | (1 << kCGEventRightMouseUp) | + (1 << kCGEventOtherMouseDown) | (1 << kCGEventOtherMouseUp) | + (1 << kCGEventLeftMouseDragged) | (1 << kCGEventRightMouseDragged) | + (1 << kCGEventOtherMouseDragged) | + (1 << kCGEventMouseMoved) | (1 << kCGEventScrollWheel); + + g_eventTap = CGEventTapCreate(kCGHIDEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault, + eventMask, MyEventTapCallback, NULL); + if (g_eventTap) { + g_runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, g_eventTap, 0); + CFRunLoopAddSource(CFRunLoopGetMain(), g_runLoopSource, kCFRunLoopCommonModes); + CGEventTapEnable(g_eventTap, true); + success = true; + } else { + NSLog(@"MacSetPrivacyMode: Failed to create CGEventTap; input blocking not enabled."); + success = false; + } + }; + + // Execute on main thread to ensure CFRunLoop operations are safe. + // Use dispatch_sync if not on main thread, otherwise execute directly to avoid deadlock. + // + // IMPORTANT: Potential deadlock consideration: + // Using dispatch_sync while holding g_privacyModeMutex could deadlock if the main thread + // tries to acquire g_privacyModeMutex. Currently this is safe because: + // 1. MacSetPrivacyMode (which holds the mutex) is only called from background threads + // 2. The main thread never directly calls MacSetPrivacyMode + // If this assumption changes in the future, consider releasing the mutex before dispatch_sync + // or restructuring the locking strategy. + if ([NSThread isMainThread]) { + setupBlock(); + } else { + dispatch_sync(dispatch_get_main_queue(), setupBlock); + } + + return success; +} + +// Helper function to tear down EventTap on the main thread +static void TeardownEventTapOnMainThread() { + void (^teardownBlock)(void) = ^{ + if (g_eventTap) { + CGEventTapEnable(g_eventTap, false); + CFRunLoopRemoveSource(CFRunLoopGetMain(), g_runLoopSource, kCFRunLoopCommonModes); + CFRelease(g_runLoopSource); + CFRelease(g_eventTap); + g_eventTap = NULL; + g_runLoopSource = NULL; + } + }; + + // Execute on main thread to ensure CFRunLoop operations are safe. + // + // NOTE: We use dispatch_sync here instead of dispatch_async because: + // 1. TurnOffPrivacyModeInternal() expects EventTap to be fully torn down before + // proceeding with gamma restoration - using async would cause race conditions. + // 2. The caller (MacSetPrivacyMode) needs deterministic cleanup order. + // + // IMPORTANT: Potential deadlock consideration (same as SetupEventTapOnMainThread): + // Using dispatch_sync while holding g_privacyModeMutex could deadlock if the main thread + // tries to acquire g_privacyModeMutex. Currently this is safe because: + // 1. MacSetPrivacyMode (which holds the mutex) is only called from background threads + // 2. The main thread never directly calls MacSetPrivacyMode + // If this assumption changes in the future, consider releasing the mutex before dispatch_sync + // or restructuring the locking strategy. + if ([NSThread isMainThread]) { + teardownBlock(); + } else { + dispatch_sync(dispatch_get_main_queue(), teardownBlock); + } +} + +// Internal function to turn off privacy mode without acquiring the mutex +// Must be called while holding g_privacyModeMutex +static bool TurnOffPrivacyModeInternal() { + if (!g_privacyModeActive) { + return true; + } + + // 1. Unregister display reconfiguration callback + CGDisplayRemoveReconfigurationCallback(DisplayReconfigurationCallback, NULL); + + // 2. Input - restore (tear down EventTap on main thread) + TeardownEventTapOnMainThread(); + + // 3. Gamma - restore using UUID to find current DisplayID + bool restoreSuccess = RestoreAllGammas(); + + // 4. Fallback: Always call CGDisplayRestoreColorSyncSettings as a safety net + // This ensures displays return to normal even if our restoration failed or + // if the system (ColorSync/Night Shift) modified gamma during privacy mode + CGDisplayRestoreColorSyncSettings(); + + // Clean up + g_originalGammas.clear(); + g_privacyModeActive = false; + g_privacyModeShutdownRequested = false; + g_lastReconfigTimestamp = 0; + g_blackoutReapplicationScheduled = false; + + return restoreSuccess; +} + +extern "C" bool MacSetPrivacyMode(bool on) { + std::lock_guard lock(g_privacyModeMutex); + if (on) { + // Already in privacy mode + if (g_privacyModeActive) { + return true; + } + + // 1. Input Blocking - set up EventTap on main thread + if (!SetupEventTapOnMainThread()) { + return false; + } + + // 2. Register display reconfiguration callback to handle hot-plug events + CGDisplayRegisterReconfigurationCallback(DisplayReconfigurationCallback, NULL); + + // 3. Gamma Blackout + uint32_t count = 0; + CGGetOnlineDisplayList(0, NULL, &count); + std::vector displays(count); + CGGetOnlineDisplayList(count, displays.data(), &count); + + uint32_t blackoutSuccessCount = 0; + uint32_t blackoutAttemptCount = 0; + + for (uint32_t i = 0; i < count; i++) { + CGDirectDisplayID d = displays[i]; + std::string uuid = GetDisplayUUID(d); + + if (uuid.empty()) { + NSLog(@"MacSetPrivacyMode: Failed to get UUID for display %u, privacy mode requires all displays", (unsigned)d); + // Privacy mode requires ALL connected displays to be successfully blacked out + // to ensure user privacy. If we can't identify a display (no UUID), + // we can't safely manage its state or restore it later. + // Therefore, we must abort the entire operation and clean up any resources + // already allocated (like event taps and reconfiguration callbacks). + CGDisplayRemoveReconfigurationCallback(DisplayReconfigurationCallback, NULL); + TeardownEventTapOnMainThread(); + // Restore gamma for displays that were already blacked out before this failure + if (!RestoreAllGammas()) { + // If any display failed to restore, use system reset as fallback + CGDisplayRestoreColorSyncSettings(); + } + g_originalGammas.clear(); + return false; + } + + // Save original gamma using UUID as key (stable across reconnections) + if (g_originalGammas.find(uuid) == g_originalGammas.end()) { + uint32_t capacity = CGDisplayGammaTableCapacity(d); + if (capacity > 0) { + std::vector red(capacity), green(capacity), blue(capacity); + uint32_t sampleCount = 0; + if (CGGetDisplayTransferByTable(d, capacity, red.data(), green.data(), blue.data(), &sampleCount) == kCGErrorSuccess) { + std::vector all; + all.insert(all.end(), red.begin(), red.begin() + sampleCount); + all.insert(all.end(), green.begin(), green.begin() + sampleCount); + all.insert(all.end(), blue.begin(), blue.begin() + sampleCount); + g_originalGammas[uuid] = all; + } else { + NSLog(@"MacSetPrivacyMode: Failed to get gamma table for display %u (UUID: %s)", (unsigned)d, uuid.c_str()); + } + } else { + NSLog(@"MacSetPrivacyMode: Display %u (UUID: %s) has zero gamma table capacity, not supported", (unsigned)d, uuid.c_str()); + } + } + + // Set to black only if we have saved original gamma for this display + if (g_originalGammas.find(uuid) != g_originalGammas.end()) { + uint32_t capacity = CGDisplayGammaTableCapacity(d); + if (capacity > 0) { + std::vector zeros(capacity, 0.0f); + blackoutAttemptCount++; + CGError error = CGSetDisplayTransferByTable(d, capacity, zeros.data(), zeros.data(), zeros.data()); + if (error != kCGErrorSuccess) { + std::string displayName = GetDisplayName(d); + NSLog(@"MacSetPrivacyMode: Failed to blackout display (Name: %s, ID: %u, UUID: %s, error: %d)", displayName.c_str(), (unsigned)d, uuid.c_str(), error); + } else { + blackoutSuccessCount++; + } + } else { + NSLog(@"MacSetPrivacyMode: Display %u (UUID: %s) has zero gamma table capacity for blackout", (unsigned)d, uuid.c_str()); + } + } + } + + // Return false if any display failed to blackout - privacy mode requires ALL displays to be blacked out + if (blackoutAttemptCount > 0 && blackoutSuccessCount < blackoutAttemptCount) { + NSLog(@"MacSetPrivacyMode: Failed to blackout all displays (%u/%u succeeded)", blackoutSuccessCount, blackoutAttemptCount); + // Clean up: unregister callback and disable event tap since we're failing + CGDisplayRemoveReconfigurationCallback(DisplayReconfigurationCallback, NULL); + TeardownEventTapOnMainThread(); + // Restore gamma for displays that were successfully blacked out + if (!RestoreAllGammas()) { + // If any display failed to restore, use system reset as fallback + NSLog(@"Some displays failed to restore gamma during cleanup, using CGDisplayRestoreColorSyncSettings as fallback"); + CGDisplayRestoreColorSyncSettings(); + } + g_originalGammas.clear(); + return false; + } + + g_privacyModeActive = true; + return true; + + } else { + return TurnOffPrivacyModeInternal(); + } +} diff --git a/src/privacy_mode.rs b/src/privacy_mode.rs index adfe25294..234004d15 100644 --- a/src/privacy_mode.rs +++ b/src/privacy_mode.rs @@ -23,6 +23,9 @@ pub mod win_mag; #[cfg(windows)] pub mod win_topmost_window; +#[cfg(target_os = "macos")] +pub mod macos; + #[cfg(windows)] mod win_virtual_display; #[cfg(windows)] @@ -105,7 +108,14 @@ lazy_static::lazy_static! { } #[cfg(not(windows))] { - "".to_owned() + #[cfg(target_os = "macos")] + { + macos::PRIVACY_MODE_IMPL.to_owned() + } + #[cfg(not(target_os = "macos"))] + { + "".to_owned() + } } }; @@ -127,7 +137,13 @@ pub type PrivacyModeCreator = fn(impl_key: &str) -> Box; lazy_static::lazy_static! { static ref PRIVACY_MODE_CREATOR: Arc>> = { #[cfg(not(windows))] - let map: HashMap<&'static str, PrivacyModeCreator> = HashMap::new(); + let mut map: HashMap<&'static str, PrivacyModeCreator> = HashMap::new(); + #[cfg(target_os = "macos")] + { + map.insert(macos::PRIVACY_MODE_IMPL, |impl_key: &str| { + Box::new(macos::PrivacyModeImpl::new(impl_key)) + }); + } #[cfg(windows)] let mut map: HashMap<&'static str, PrivacyModeCreator> = HashMap::new(); #[cfg(windows)] @@ -333,7 +349,14 @@ pub fn get_supported_privacy_mode_impl() -> Vec<(&'static str, &'static str)> { vec_impls } - #[cfg(not(target_os = "windows"))] + #[cfg(target_os = "macos")] + { + // No translation is intended for privacy_mode_impl_macos_tip as it is a + // placeholder for macOS specific privacy mode implementation which currently + // doesn't provide multiple modes like Windows does. + vec![(macos::PRIVACY_MODE_IMPL, "privacy_mode_impl_macos_tip")] + } + #[cfg(not(any(target_os = "windows", target_os = "macos")))] { Vec::new() } diff --git a/src/privacy_mode/macos.rs b/src/privacy_mode/macos.rs new file mode 100644 index 000000000..e6ea11e49 --- /dev/null +++ b/src/privacy_mode/macos.rs @@ -0,0 +1,81 @@ +use super::{PrivacyMode, PrivacyModeState}; +use hbb_common::{anyhow::anyhow, ResultType}; + +extern "C" { + fn MacSetPrivacyMode(on: bool) -> bool; +} + +pub const PRIVACY_MODE_IMPL: &str = "privacy_mode_impl_macos"; + +pub struct PrivacyModeImpl { + impl_key: String, + conn_id: i32, +} + +impl PrivacyModeImpl { + pub fn new(impl_key: &str) -> Self { + Self { + impl_key: impl_key.to_owned(), + conn_id: 0, + } + } +} + +impl PrivacyMode for PrivacyModeImpl { + fn is_async_privacy_mode(&self) -> bool { + false + } + + fn init(&self) -> ResultType<()> { + Ok(()) + } + + fn clear(&mut self) { + unsafe { + MacSetPrivacyMode(false); + } + self.conn_id = 0; + } + + fn turn_on_privacy(&mut self, conn_id: i32) -> ResultType { + if self.check_on_conn_id(conn_id)? { + return Ok(true); + } + let success = unsafe { MacSetPrivacyMode(true) }; + if !success { + return Err(anyhow!("Failed to turn on privacy mode")); + } + self.conn_id = conn_id; + Ok(true) + } + + fn turn_off_privacy(&mut self, conn_id: i32, _state: Option) -> ResultType<()> { + // Note: The `_state` parameter is intentionally ignored on macOS. + // On Windows, it's used to notify the connection manager about privacy mode state changes + // (see win_topmost_window.rs). macOS currently has a simpler single-mode implementation + // without the need for such cross-component state synchronization. + self.check_off_conn_id(conn_id)?; + let success = unsafe { MacSetPrivacyMode(false) }; + if !success { + return Err(anyhow!("Failed to turn off privacy mode")); + } + self.conn_id = 0; + Ok(()) + } + + fn pre_conn_id(&self) -> i32 { + self.conn_id + } + + fn get_impl_key(&self) -> &str { + &self.impl_key + } +} + +impl Drop for PrivacyModeImpl { + fn drop(&mut self) { + // Use the same cleanup logic as other code paths to keep conn_id consistent + // and ensure all cleanup is centralized in one place. + self.clear(); + } +} diff --git a/src/server/connection.rs b/src/server/connection.rs index f90aad115..10b578042 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1420,7 +1420,7 @@ impl Connection { pi.platform = "Android".into(); } #[cfg(all(target_os = "macos", not(feature = "unix-file-copy-paste")))] - let platform_additions = serde_json::Map::new(); + let mut platform_additions = serde_json::Map::new(); #[cfg(any( target_os = "windows", target_os = "linux", @@ -1453,6 +1453,13 @@ impl Connection { json!(privacy_mode::get_supported_privacy_mode_impl()), ); } + #[cfg(target_os = "macos")] + { + platform_additions.insert( + "supported_privacy_mode_impl".into(), + json!(privacy_mode::get_supported_privacy_mode_impl()), + ); + } #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] { From 56a8f6b97b7508fa38f05fe188c3969961288ca2 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 28 Jan 2026 15:11:44 +0800 Subject: [PATCH 107/277] fix(iOS): Unexpected mouse movement to (0,0) on idle (#14180) Signed-off-by: fufesou --- flutter/lib/models/input_model.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index c14a23739..0eb74dbc5 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -1048,6 +1048,14 @@ class InputModel { if (isViewOnly && !showMyCursor) return; if (e.kind != ui.PointerDeviceKind.mouse) return; + // May fix https://github.com/rustdesk/rustdesk/issues/13009 + if (isIOS && e.synthesized && e.position == Offset.zero && e.buttons == 0) { + // iOS may emit a synthesized hover event at (0,0) when the mouse is disconnected. + // Ignore this event to prevent cursor jumping. + debugPrint('Ignored synthesized hover at (0,0) on iOS'); + return; + } + // Only update pointer region when relative mouse mode is enabled. // This avoids unnecessary tracking when not in relative mode. if (_relativeMouse.enabled.value) { From 216ec9d52b109eeee888800159498073a9dfb729 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 28 Jan 2026 15:12:42 +0800 Subject: [PATCH 108/277] fix(terminal): ios delete (#14147) Signed-off-by: fufesou --- flutter/lib/mobile/pages/terminal_page.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/flutter/lib/mobile/pages/terminal_page.dart b/flutter/lib/mobile/pages/terminal_page.dart index a0064f068..67d77782f 100644 --- a/flutter/lib/mobile/pages/terminal_page.dart +++ b/flutter/lib/mobile/pages/terminal_page.dart @@ -164,6 +164,13 @@ class _TerminalPageState extends State autofocus: true, textStyle: _getTerminalStyle(), backgroundOpacity: 0.7, + // The following comment is from xterm.dart source code: + // Workaround to detect delete key for platforms and IMEs that do not + // emit a hardware delete event. Preferred on mobile platforms. [false] by + // default. + // + // Android works fine without this workaround. + deleteDetection: isIOS, padding: _calculatePadding(heightPx), onSecondaryTapDown: (details, offset) async { final selection = _terminalModel.terminalController.selection; From 45cab7f808171a23d14a55ae25b4a52d2e361053 Mon Sep 17 00:00:00 2001 From: ThallesWS Date: Wed, 28 Jan 2026 04:14:06 -0300 Subject: [PATCH 109/277] fix issue: #13911 'Double Click' bug on iPad with Magic Mouse (#14086) * fix issue: #13911 'Double Click' bug on iPad with Magic Mouse * remote_input.dart comments - gestures.dart organization and clean states of all interrupted gestures --- flutter/lib/common/widgets/gestures.dart | 24 ++++++++++++++++++++ flutter/lib/common/widgets/remote_input.dart | 12 +++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/flutter/lib/common/widgets/gestures.dart b/flutter/lib/common/widgets/gestures.dart index 74b1642b7..0501ca453 100644 --- a/flutter/lib/common/widgets/gestures.dart +++ b/flutter/lib/common/widgets/gestures.dart @@ -25,6 +25,7 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer { GestureDragStartCallback? onOneFingerPanStart; GestureDragUpdateCallback? onOneFingerPanUpdate; GestureDragEndCallback? onOneFingerPanEnd; + GestureDragCancelCallback? onOneFingerPanCancel; // twoFingerScale : scale + pan event GestureScaleStartCallback? onTwoFingerScaleStart; @@ -169,6 +170,27 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer { DragEndDetails _getDragEndDetails(ScaleEndDetails d) => DragEndDetails(velocity: d.velocity); + + @override + void rejectGesture(int pointer) { + super.rejectGesture(pointer); + switch (_currentState) { + case GestureState.oneFingerPan: + if (onOneFingerPanCancel != null) { + onOneFingerPanCancel!(); + } + break; + case GestureState.twoFingerScale: + // Reset scale state if needed, currently self-contained + break; + case GestureState.threeFingerVerticalDrag: + // Reset drag state if needed, currently self-contained + break; + default: + break; + } + _currentState = GestureState.none; + } } class HoldTapMoveGestureRecognizer extends GestureRecognizer { @@ -717,6 +739,7 @@ RawGestureDetector getMixinGestureDetector({ GestureDragStartCallback? onOneFingerPanStart, GestureDragUpdateCallback? onOneFingerPanUpdate, GestureDragEndCallback? onOneFingerPanEnd, + GestureDragCancelCallback? onOneFingerPanCancel, GestureScaleUpdateCallback? onTwoFingerScaleUpdate, GestureScaleEndCallback? onTwoFingerScaleEnd, GestureDragUpdateCallback? onThreeFingerVerticalDragUpdate, @@ -765,6 +788,7 @@ RawGestureDetector getMixinGestureDetector({ ..onOneFingerPanStart = onOneFingerPanStart ..onOneFingerPanUpdate = onOneFingerPanUpdate ..onOneFingerPanEnd = onOneFingerPanEnd + ..onOneFingerPanCancel = onOneFingerPanCancel ..onTwoFingerScaleUpdate = onTwoFingerScaleUpdate ..onTwoFingerScaleEnd = onTwoFingerScaleEnd ..onThreeFingerVerticalDragUpdate = onThreeFingerVerticalDragUpdate; diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index 95a716042..2c97ea147 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -158,7 +158,8 @@ class _RawTouchGestureDetectorRegionState final isMoved = await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); if (isMoved) { - if (lastTapDownDetails != null) { + // If pan already handled 'down', don't send it again. + if (lastTapDownDetails != null && !_touchModePanStarted) { await inputModel.tapDown(MouseButtons.left); } await inputModel.tapUp(MouseButtons.left); @@ -424,6 +425,14 @@ class _RawTouchGestureDetectorRegionState } } + // Reset `_touchModePanStarted` if the one-finger pan gesture is cancelled + // or rejected by the gesture arena. Without this, the flag can remain + // stuck in the "started" state and cause issues such as the Magic Mouse + // double-click problem on iPad with magic mouse. + onOneFingerPanCancel() { + _touchModePanStarted = false; + } + // scale + pan event onTwoFingerScaleStart(ScaleStartDetails d) { _lastTapDownDetails = null; @@ -557,6 +566,7 @@ class _RawTouchGestureDetectorRegionState instance ..onOneFingerPanUpdate = onOneFingerPanUpdate ..onOneFingerPanEnd = onOneFingerPanEnd + ..onOneFingerPanCancel = onOneFingerPanCancel ..onTwoFingerScaleStart = onTwoFingerScaleStart ..onTwoFingerScaleUpdate = onTwoFingerScaleUpdate ..onTwoFingerScaleEnd = onTwoFingerScaleEnd From f112d097dcd8b78d7c8ca251d713d820f2618afc Mon Sep 17 00:00:00 2001 From: John Fowler Date: Wed, 28 Jan 2026 08:15:29 +0100 Subject: [PATCH 110/277] Replacing incorrect quotation marks (#14144) * Update Hungarian translations in hu.rs Translation of new strings and some fixes. John Fowler. * Escape quotes in Hungarian language strings Replacing Hungarian quotation marks * Update Hungarian translations for various terms Upload a new translation (hu.rs) file. --- src/lang/hu.rs | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 609773681..c06400cbf 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -149,7 +149,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Click to upgrade", "Kattintson ide a frissítés telepítéséhez"), ("Configure", "Beállítás"), ("config_acc", "A számítógép távoli vezérléséhez a RustDesknek hozzáférési jogokat kell adnia."), - ("config_screen", "Ahhoz, hogy távolról hozzáférhessen a számítógépéhez, meg kell adnia a RustDesknek a „Képernyőfelvétel” jogosultságot."), + ("config_screen", "Ahhoz, hogy távolról hozzáférhessen a számítógépéhez, meg kell adnia a RustDesknek a \"Képernyőfelvétel\" jogosultságot."), ("Installing ...", "Telepítés…"), ("Install", "Telepítés"), ("Installation", "Telepítés"), @@ -276,13 +276,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Do you accept?", "Elfogadás?"), ("Open System Setting", "Rendszerbeállítások megnyitása"), ("How to get Android input permission?", "Hogyan állítható be az Androidos beviteli engedély?"), - ("android_input_permission_tip1", "Ahhoz, hogy egy távoli eszköz vezérelhesse Android készülékét, engedélyeznie kell a RustDesk számára a „Hozzáférhetőség” szolgáltatás használatát."), + ("android_input_permission_tip1", "Ahhoz, hogy egy távoli eszköz vezérelhesse Android készülékét, engedélyeznie kell a RustDesk számára a \"Hozzáférhetőség\" szolgáltatás használatát."), ("android_input_permission_tip2", "A következő rendszerbeállítások oldalon a letöltött alkalmazások menüponton belül, kapcsolja be a [RustDesk Input] szolgáltatást."), ("android_new_connection_tip", "Új kérés érkezett, mely vezérelni szeretné az eszközét"), ("android_service_will_start_tip", "A képernyőmegosztás aktiválása automatikusan elindítja a szolgáltatást, így más eszközök is vezérelhetik ezt az Android-eszközt."), ("android_stop_service_tip", "A szolgáltatás leállítása automatikusan szétkapcsol minden létező kapcsolatot."), ("android_version_audio_tip", "A jelenlegi Android verzió nem támogatja a hangrögzítést, frissítsen legalább Android 10-re, vagy egy újabb verzióra."), - ("android_start_service_tip", "A képernyőmegosztó szolgáltatás elindításához koppintson a „Kapcsolási szolgáltatás indítása” gombra, vagy aktiválja a „Képernyőfelvétel” engedélyt."), + ("android_start_service_tip", "A képernyőmegosztó szolgáltatás elindításához koppintson a \"Kapcsolási szolgáltatás indítása\" gombra, vagy aktiválja a \"Képernyőfelvétel\" engedélyt."), ("android_permission_may_not_change_tip", "A meglévő kapcsolatok engedélyei csak új kapcsolódás után módosulnak."), ("Account", "Fiók"), ("Overwrite", "Felülírás"), @@ -408,15 +408,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", "Helyi billentyűzet típusának kiválasztása"), ("software_render_tip", "Ha Nvidia grafikus kártyát használ Linux alatt, és a távoli ablak a kapcsolat létrehozása után azonnal bezáródik, akkor a Nouveau nyílt forráskódú illesztőprogramra való váltás és a szoftveres leképezés alkalmazása segíthet. A szoftvert újra kell indítani."), ("Always use software rendering", "Mindig szoftveres leképezést használjon"), - ("config_input", "Ahhoz, hogy a távoli asztalt a billentyűzettel vezérelhesse, a RustDesknek meg kell adnia a „Bemenet figyelése” jogosultságot."), - ("config_microphone", "Ahhoz, hogy távolról beszélhessen, meg kell adnia a RustDesknek a „Hangfelvétel” jogosultságot."), + ("config_input", "Ahhoz, hogy a távoli asztalt a billentyűzettel vezérelhesse, a RustDesknek meg kell adnia a \"Bemenet figyelése\" jogosultságot."), + ("config_microphone", "Ahhoz, hogy távolról beszélhessen, meg kell adnia a RustDesknek a \"Hangfelvétel\" jogosultságot."), ("request_elevation_tip", "Akkor is kérhet megnövelt jogokat, ha valaki a partneroldalon van."), ("Wait", "Várjon"), ("Elevation Error", "Emelt szintű hozzáférési hiba"), ("Ask the remote user for authentication", "Hitelesítés kérése a távoli felhasználótól"), ("Choose this if the remote account is administrator", "Akkor válassza ezt, ha a távoli fiók rendszergazda"), ("Transmit the username and password of administrator", "Küldje el a rendszergazda felhasználónevét és jelszavát"), - ("still_click_uac_tip", "A távoli felhasználónak továbbra is az „Igen” gombra kell kattintania a RustDesk UAC ablakában. Kattintson!"), + ("still_click_uac_tip", "A távoli felhasználónak továbbra is az \"Igen\" gombra kell kattintania a RustDesk UAC ablakában. Kattintson!"), ("Request Elevation", "Emelt szintű jogok igénylése"), ("wait_accept_uac_tip", "Várjon, amíg a távoli felhasználó elfogadja az UAC párbeszédet."), ("Elevate successfully", "Emelt szintű jogok megadva"), @@ -442,7 +442,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Hanghívás"), ("Text chat", "Szöveges csevegés"), ("Stop voice call", "Hanghívás leállítása"), - ("relay_hint_tip", "Ha a közvetlen kapcsolat nem lehetséges, megpróbálhat kapcsolatot létesíteni egy továbbító-kiszolgálón keresztül.\nHa az első próbálkozáskor továbbító-kiszolgálón keresztüli kapcsolatot szeretne létrehozni, használhatja az „/r” utótagot. Az azonosítóhoz vagy a „Mindig továbbító-kiszolgálón keresztül kapcsolódom” opcióhoz a legutóbbi munkamenetek listájában, ha van ilyen."), + ("relay_hint_tip", "Ha a közvetlen kapcsolat nem lehetséges, megpróbálhat kapcsolatot létesíteni egy továbbító-kiszolgálón keresztül.\nHa az első próbálkozáskor továbbító-kiszolgálón keresztüli kapcsolatot szeretne létrehozni, használhatja az \"/r\" utótagot. Az azonosítóhoz vagy a \"Mindig továbbító-kiszolgálón keresztül kapcsolódom\" opcióhoz a legutóbbi munkamenetek listájában, ha van ilyen."), ("Reconnect", "Újrakapcsolódás"), ("Codec", "Kodek"), ("Resolution", "Felbontás"), @@ -490,7 +490,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update", "Frissítés"), ("Enable", "Engedélyezés"), ("Disable", "Letiltás"), - ("Options", "Beállítások"), + ("Options", "Opciók"), ("resolution_original_tip", "Eredeti felbontás"), ("resolution_fit_local_tip", "Helyi felbontás beállítása"), ("resolution_custom_tip", "Testre szabható felbontás"), @@ -559,7 +559,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Plug out all", "Kapcsolja ki az összeset"), ("True color (4:4:4)", "Valódi szín (4:4:4)"), ("Enable blocking user input", "Engedélyezze a felhasználói bevitel blokkolását"), - ("id_input_tip", "Megadhat egy azonosítót, egy közvetlen IP-címet vagy egy tartományt egy porttal (:).\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (@?key=), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a „@public” lehetőséget. A kulcsra nincs szükség nyilvános kiszolgálók esetén.\n\nHa az első kapcsolathoz továbbító-kiszolgálón keresztüli kapcsolatot akar kényszeríteni, adja hozzá az „/r” az azonosítót a végén, például „9123456234/r”."), + ("id_input_tip", "Megadhat egy azonosítót, egy közvetlen IP-címet vagy egy tartományt egy porttal (:).\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (@?key=), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a \"@public\" lehetőséget. A kulcsra nincs szükség nyilvános kiszolgálók esetén.\n\nHa az első kapcsolathoz továbbító-kiszolgálón keresztüli kapcsolatot akar kényszeríteni, adja hozzá az \"/r\" az azonosítót a végén, például \"9123456234/r\"."), ("privacy_mode_impl_mag_tip", "1. mód"), ("privacy_mode_impl_virtual_display_tip", "2. mód"), ("Enter privacy mode", "Lépjen be az adatvédelmi módba"), @@ -622,7 +622,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Power", "Főkapcsoló"), ("Telegram bot", "Telegram bot"), ("enable-bot-tip", "Ha aktiválja ezt a funkciót, akkor a 2FA-kódot a botjától kaphatja meg. Kapcsolati értesítésként is használható."), - ("enable-bot-desc", "1. Nyisson csevegést @BotFather.\n2. Küldje el a „/newbot” parancsot. Miután ezt a lépést elvégezte, kap egy tokent.\n3. Indítson csevegést az újonnan létrehozott botjával. Küldjön egy olyan üzenetet, amely egy perjel („/”) kezdetű, pl. „/hello” az aktiváláshoz.\n"), + ("enable-bot-desc", "1. Nyisson csevegést @BotFather.\n2. Küldje el a \"/newbot\" parancsot. Miután ezt a lépést elvégezte, kap egy tokent.\n3. Indítson csevegést az újonnan létrehozott botjával. Küldjön egy olyan üzenetet, amely egy perjel (\"/\") kezdetű, pl. \"/hello\" az aktiváláshoz.\n"), ("cancel-2fa-confirm-tip", "Biztosan vissza akarja vonni a 2FA-hitelesítést?"), ("cancel-bot-confirm-tip", "Biztosan le akarja mondani a Telegram botot?"), ("About RustDesk", "A RustDesk névjegye"), @@ -643,7 +643,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", "Az egyirányú fájlátvitel engedélyezve van a vezérelt oldalon."), ("Authentication Required", "Hitelesítés szükséges"), ("Authenticate", "Hitelesítés"), - ("web_id_input_tip", "Azonos kiszolgálón lévő azonosítót adhat meg, a közvetlen IP elérés nem támogatott a webkliensben.\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (@?key=), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a „@public” betűt. A kulcsra nincs szükség a nyilvános kiszolgálók esetében."), + ("web_id_input_tip", "Azonos kiszolgálón lévő azonosítót adhat meg, a közvetlen IP elérés nem támogatott a webkliensben.\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (@?key=), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a \"@public\" betűt. A kulcsra nincs szükség a nyilvános kiszolgálók esetében."), ("Download", "Letöltés"), ("Upload folder", "Mappa feltöltése"), ("Upload files", "Fájlok feltöltése"), @@ -682,9 +682,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Downloading {}", "{} letöltése"), ("{} Update", "{} frissítés"), ("{}-to-update-tip", "A(z) {} bezárása és az új verzió telepítése."), - ("download-new-version-failed-tip", "Ha a letöltés sikertelen, akkor vagy újrapróbálkozhat, vagy a „Letöltés” gombra kattintva letöltheti a kiadási oldalról, és manuálisan frissíthet."), + ("download-new-version-failed-tip", "Ha a letöltés sikertelen, akkor vagy újrapróbálkozhat, vagy a \"Letöltés\" gombra kattintva letöltheti a kiadási oldalról, és manuálisan frissíthet."), ("Auto update", "Automatikus frissítés"), - ("update-failed-check-msi-tip", "A telepítési módszer felismerése nem sikerült. Kattintson a „Letöltés” gombra, hogy letöltse a kiadási oldalról, és manuálisan frissítse."), + ("update-failed-check-msi-tip", "A telepítési módszer felismerése nem sikerült. Kattintson a \"Letöltés\" gombra, hogy letöltse a kiadási oldalról, és manuálisan frissítse."), ("websocket_tip", "WebSocket használatakor csak a relé-kapcsolatok támogatottak."), ("Use WebSocket", "WebSocket használata"), ("Trackpad speed", "Érintőpad sebessége"), @@ -730,14 +730,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", "Megjegyzés beírása"), ("note-at-conn-end-tip", "Kérjen megjegyzést a kapcsolat végén"), ("Show terminal extra keys", "További terminálgombok megjelenítése"), - ("Relative mouse mode", "Relatív egérmód"), - ("rel-mouse-not-supported-peer-tip", "A kapcsolódott partner nem támogatja a relatív egérmódot."), - ("rel-mouse-not-ready-tip", "A relatív egérmód még nem elérhető. Próbálja meg újra."), - ("rel-mouse-lock-failed-tip", "Nem sikerült zárolni a kurzort. A relatív egérmód le lett tiltva."), + ("Relative mouse mode", "Relatív egér mód"), + ("rel-mouse-not-supported-peer-tip", "A kapcsolódott partner nem támogatja a relatív egér módot."), + ("rel-mouse-not-ready-tip", "A relatív egér mód még nem elérhető. Próbálja meg újra."), + ("rel-mouse-lock-failed-tip", "Nem sikerült zárolni a kurzort. A relatív egér mód le lett tiltva."), ("rel-mouse-exit-{}-tip", "A kilépéshez nyomja meg a(z) {} gombot."), - ("rel-mouse-permission-lost-tip", "A billentyűzet-hozzáférés vissza lett vonva. A relatív egérmód le lett tilva."), + ("rel-mouse-permission-lost-tip", "A billentyűzet-hozzáférés vissza lett vonva. A relatív egér mód le lett tilva."), ("Changelog", "Változáslista"), - ("keep-awake-during-outgoing-sessions-label", ""), - ("keep-awake-during-incoming-sessions-label", ""), + ("keep-awake-during-outgoing-sessions-label", "Képernyő aktív állapotban tartása a kimenő munkamenetek során"), + ("keep-awake-during-incoming-sessions-label", "Képernyő aktív állapotban tartása a bejövő munkamenetek során"), ].iter().cloned().collect(); } From 1a90e6b6c7ab1f40e12d2ca0bb12e396bba94c31 Mon Sep 17 00:00:00 2001 From: Lynilia <89228568+Lynilia@users.noreply.github.com> Date: Wed, 28 Jan 2026 08:16:06 +0100 Subject: [PATCH 111/277] Update fr.rs (#14151) --- src/lang/fr.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/fr.rs b/src/lang/fr.rs index a5deb4596..9b56726d5 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -106,7 +106,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to delete this empty directory?", "Voulez-vous vraiment supprimer ce répertoire vide ?"), ("Are you sure you want to delete the file of this directory?", "Voulez-vous vraiment supprimer le fichier de ce répertoire ?"), ("Do this for all conflicts", "Appliquer à tous les conflits"), - ("This is irreversible!", "Ceci est irréversible !"), + ("This is irreversible!", "Cette action est irréversible !"), ("Deleting", "Suppression"), ("files", "fichiers"), ("Waiting", "En attente"), @@ -737,7 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", "Appuyez sur {} pour quitter."), ("rel-mouse-permission-lost-tip", "L’autorisation de contrôle du clavier a été révoquée. Le mode souris relative a été désactivé."), ("Changelog", "Journal des modifications"), - ("keep-awake-during-outgoing-sessions-label", ""), - ("keep-awake-during-incoming-sessions-label", ""), + ("keep-awake-during-outgoing-sessions-label", "Maintenir l’écran allumé lors des sessions sortantes"), + ("keep-awake-during-incoming-sessions-label", "Maintenir l’écran allumé lors des sessions entrantes"), ].iter().cloned().collect(); } From 5f3ceef5922242e38d0d0a78527a27cc7fac2fa6 Mon Sep 17 00:00:00 2001 From: twprh <46543715+twprh@users.noreply.github.com> Date: Wed, 28 Jan 2026 08:16:27 +0100 Subject: [PATCH 112/277] Update de.rs (#14139) zum Zeitpunkt der Anzeige ist der Datenschutz aktiviert bzw. schon beendet. alternativ ginge auch: Datenschutzmodus wurde aktiviert bzw. Datenschutzmodus wurde beendet --- src/lang/de.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index f77c3cc97..b0757e223 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -562,8 +562,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("id_input_tip", "Sie können eine ID, eine direkte IP oder eine Domäne mit einem Port (:) eingeben.\nWenn Sie auf ein Gerät auf einem anderen Server zugreifen wollen, fügen Sie bitte die Serveradresse (@?key=) hinzu, zum Beispiel\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nWenn Sie auf ein Gerät auf einem öffentlichen Server zugreifen wollen, geben Sie bitte \"@public\" ein. Der Schlüssel wird für öffentliche Server nicht benötigt.\n\nWenn Sie bei der ersten Verbindung die Verwendung einer Relay-Verbindung erzwingen wollen, fügen Sie \"/r\" am Ende der ID hinzu, zum Beispiel \"9123456234/r\"."), ("privacy_mode_impl_mag_tip", "Modus 1"), ("privacy_mode_impl_virtual_display_tip", "Modus 2"), - ("Enter privacy mode", "Datenschutzmodus aktivieren"), - ("Exit privacy mode", "Datenschutzmodus beenden"), + ("Enter privacy mode", "Datenschutzmodus aktiviert"), + ("Exit privacy mode", "Datenschutzmodus beendet"), ("idd_not_support_under_win10_2004_tip", "Indirekter Grafiktreiber wird nicht unterstützt. Windows 10, Version 2004 oder neuer ist erforderlich."), ("input_source_1_tip", "Eingangsquelle 1"), ("input_source_2_tip", "Eingangsquelle 2"), From 79ef4c4501b0d9ce271cb033b63c26cefcbd872d Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:44:17 +0800 Subject: [PATCH 113/277] Copilot/fix action run error (#14186) * Initial plan * Fix macOS build: Remove @available check causing linker error The @available check in GetDisplayName was causing the linker to look for __isPlatformVersionAtLeast symbol which is not available when targeting macOS 10.14. Since this function is only used for logging, we simplify it to return "Unknown" for all displays, avoiding the runtime availability check. Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com> * fix(macOS): ___isPlatformVersionAtLeast is not available in macOS 10.14 Signed-off-by: fufesou --------- Signed-off-by: fufesou Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com> --- src/platform/macos.mm | 28 ++-------------------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/src/platform/macos.mm b/src/platform/macos.mm index a9270455b..3303855a6 100644 --- a/src/platform/macos.mm +++ b/src/platform/macos.mm @@ -357,27 +357,6 @@ static std::string GetDisplayUUID(CGDirectDisplayID displayId) { return ""; } -// Helper function to get display name from DisplayID -static std::string GetDisplayName(CGDirectDisplayID displayId) { - NSArray *screens = [NSScreen screens]; - for (NSScreen *screen in screens) { - NSDictionary *deviceDescription = [screen deviceDescription]; - NSNumber *screenNumber = [deviceDescription objectForKey:@"NSScreenNumber"]; - CGDirectDisplayID screenDisplayID = [screenNumber unsignedIntValue]; - if (screenDisplayID == displayId) { - // localizedName is available on macOS 10.15+ - if (@available(macOS 10.15, *)) { - NSString *name = [screen localizedName]; - if (name) { - return std::string([name UTF8String]); - } - } - break; - } - } - return "Unknown"; -} - // Helper function to find DisplayID by UUID from current online displays static CGDirectDisplayID FindDisplayIdByUUID(const std::string& targetUuid) { uint32_t count = 0; @@ -415,9 +394,7 @@ static bool RestoreAllGammas() { const CGGammaValue* blue = green + sampleCount; CGError error = CGSetDisplayTransferByTable(d, sampleCount, red, green, blue); if (error != kCGErrorSuccess) { - std::string displayName = GetDisplayName(d); - NSLog(@"Failed to restore gamma for display (Name: %s, ID: %u, UUID: %s, error: %d)", - displayName.c_str(), (unsigned)d, uuid.c_str(), error); + NSLog(@"Failed to restore gamma for display (ID: %u, UUID: %s, error: %d)", (unsigned)d, uuid.c_str(), error); allSuccess = false; } } @@ -897,8 +874,7 @@ extern "C" bool MacSetPrivacyMode(bool on) { blackoutAttemptCount++; CGError error = CGSetDisplayTransferByTable(d, capacity, zeros.data(), zeros.data(), zeros.data()); if (error != kCGErrorSuccess) { - std::string displayName = GetDisplayName(d); - NSLog(@"MacSetPrivacyMode: Failed to blackout display (Name: %s, ID: %u, UUID: %s, error: %d)", displayName.c_str(), (unsigned)d, uuid.c_str(), error); + NSLog(@"MacSetPrivacyMode: Failed to blackout display (ID: %u, UUID: %s, error: %d)", (unsigned)d, uuid.c_str(), error); } else { blackoutSuccessCount++; } From 1e6bfa7bb1cff873a2238ef4fbc4c655d9e74d27 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 29 Jan 2026 15:25:44 +0800 Subject: [PATCH 114/277] fix(iPad): Magic Mouse, click (#14188) Signed-off-by: fufesou --- flutter/lib/common/widgets/remote_input.dart | 12 +++++++ flutter/lib/models/input_model.dart | 34 +++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index 2c97ea147..e35da6424 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -107,6 +107,8 @@ class _RawTouchGestureDetectorRegionState // For mouse mode, we need to block the events when the cursor is in a blocked area. // So we need to cache the last tap down position. Offset? _lastTapDownPositionForMouseMode; + // Cache global position for onTap (which lacks position info). + Offset? _lastTapDownGlobalPosition; FFI get ffi => widget.ffi; FfiModel get ffiModel => widget.ffiModel; @@ -136,6 +138,7 @@ class _RawTouchGestureDetectorRegionState onTapDown(TapDownDetails d) async { lastDeviceKind = d.kind; + _lastTapDownGlobalPosition = d.globalPosition; if (isNotTouchBasedDevice()) { return; } @@ -154,6 +157,10 @@ class _RawTouchGestureDetectorRegionState if (isNotTouchBasedDevice()) { return; } + // Filter duplicate touch tap events on iOS (Magic Mouse issue). + if (inputModel.shouldIgnoreTouchTap(d.globalPosition)) { + return; + } if (handleTouch) { final isMoved = await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); @@ -171,6 +178,11 @@ class _RawTouchGestureDetectorRegionState if (isNotTouchBasedDevice()) { return; } + // Filter duplicate touch tap events on iOS (Magic Mouse issue). + final lastPos = _lastTapDownGlobalPosition; + if (lastPos != null && inputModel.shouldIgnoreTouchTap(lastPos)) { + return; + } if (!handleTouch) { // Cannot use `_lastTapDownDetails` because Flutter calls `onTapUp` before `onTap`, clearing the cached details. // Using `_lastTapDownPositionForMouseMode` instead. diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 0eb74dbc5..97ef80a55 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -826,6 +826,9 @@ class InputModel { Map _getMouseEvent(PointerEvent evt, String type) { final Map out = {}; + bool hasStaleButtonsOnMouseUp = + type == _kMouseEventUp && evt.buttons == _lastButtons; + // Check update event type and set buttons to be sent. int buttons = _lastButtons; if (type == _kMouseEventMove) { @@ -850,7 +853,7 @@ class InputModel { buttons = evt.buttons; } } - _lastButtons = evt.buttons; + _lastButtons = hasStaleButtonsOnMouseUp ? 0 : evt.buttons; out['buttons'] = buttons; out['type'] = type; @@ -1218,6 +1221,28 @@ class InputModel { _trackpadLastDelta = Offset.zero; } + // iOS Magic Mouse duplicate event detection. + // When using Magic Mouse on iPad, iOS may emit both mouse and touch events + // for the same click in certain areas (like top-left corner). + int _lastMouseDownTimeMs = 0; + ui.Offset _lastMouseDownPos = ui.Offset.zero; + + /// Check if a touch tap event should be ignored because it's a duplicate + /// of a recent mouse event (iOS Magic Mouse issue). + bool shouldIgnoreTouchTap(ui.Offset pos) { + if (!isIOS) return false; + final nowMs = DateTime.now().millisecondsSinceEpoch; + final dt = nowMs - _lastMouseDownTimeMs; + final distance = (_lastMouseDownPos - pos).distance; + // If touch tap is within 2000ms and 80px of the last mouse down, + // it's likely a duplicate event from the same Magic Mouse click. + if (dt >= 0 && dt < 2000 && distance < 80.0) { + debugPrint("shouldIgnoreTouchTap: IGNORED (dt=$dt, dist=$distance)"); + return true; + } + return false; + } + void onPointDownImage(PointerDownEvent e) { debugPrint("onPointDownImage ${e.kind}"); _stopFling = true; @@ -1227,6 +1252,13 @@ class InputModel { if (isViewOnly && !showMyCursor) return; if (isViewCamera) return; + // Track mouse down events for duplicate detection on iOS. + final nowMs = DateTime.now().millisecondsSinceEpoch; + if (e.kind == ui.PointerDeviceKind.mouse) { + _lastMouseDownTimeMs = nowMs; + _lastMouseDownPos = e.position; + } + if (_relativeMouse.enabled.value) { _relativeMouse.updatePointerRegionTopLeftGlobal(e); } From e1b1a927b8c693b047bafcc75fce09f24391cd00 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 30 Jan 2026 17:32:18 +0800 Subject: [PATCH 115/277] fix(ios): capsLock, workaround #5871 (#14194) Signed-off-by: fufesou --- flutter/lib/models/input_model.dart | 135 +++++++++++++++++++--------- 1 file changed, 94 insertions(+), 41 deletions(-) diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 97ef80a55..134b21107 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -59,7 +59,8 @@ class CanvasCoords { model.scale = json['scale']; model.scrollX = json['scrollX']; model.scrollY = json['scrollY']; - model.scrollStyle = ScrollStyle.fromJson(json['scrollStyle'], ScrollStyle.scrollauto); + model.scrollStyle = + ScrollStyle.fromJson(json['scrollStyle'], ScrollStyle.scrollauto); model.size = Size(json['size']['w'], json['size']['h']); return model; } @@ -418,6 +419,74 @@ class InputModel { }); } + // https://github.com/flutter/flutter/issues/157241 + // Infer CapsLock state from the character output. + // This is needed because Flutter's HardwareKeyboard.lockModesEnabled may report + // incorrect CapsLock state on iOS. + bool _getIosCapsFromCharacter(KeyEvent e) { + if (!isIOS) return false; + final ch = e.character; + return _getIosCapsFromCharacterImpl( + ch, HardwareKeyboard.instance.isShiftPressed); + } + + // RawKeyEvent version of _getIosCapsFromCharacter. + bool _getIosCapsFromRawCharacter(RawKeyEvent e) { + if (!isIOS) return false; + final ch = e.character; + return _getIosCapsFromCharacterImpl(ch, e.isShiftPressed); + } + + // Shared implementation for inferring CapsLock state from character. + // Uses Unicode-aware case detection to support non-ASCII letters (e.g., ü/Ü, é/É). + // + // Limitations: + // 1. This inference assumes the client and server use the same keyboard layout. + // If layouts differ (e.g., client uses EN, server uses DE), the character output + // may not match expectations. For example, ';' on EN layout maps to 'ö' on DE + // layout, making it impossible to correctly infer CapsLock state from the + // character alone. + // 2. On iOS, CapsLock+Shift produces uppercase letters (unlike desktop where it + // produces lowercase). This method cannot handle that case correctly. + bool _getIosCapsFromCharacterImpl(String? ch, bool shiftPressed) { + if (ch == null || ch.length != 1) return false; + // Use Dart's built-in Unicode-aware case detection + final upper = ch.toUpperCase(); + final lower = ch.toLowerCase(); + final isUpper = upper == ch && lower != ch; + final isLower = lower == ch && upper != ch; + // Skip non-letter characters (e.g., numbers, symbols, CJK characters without case) + if (!isUpper && !isLower) return false; + return isUpper != shiftPressed; + } + + int _buildLockModes(bool iosCapsLock) { + const capslock = 1; + const numlock = 2; + const scrolllock = 3; + int lockModes = 0; + if (isIOS) { + if (iosCapsLock) { + lockModes |= (1 << capslock); + } + // Ignore "NumLock/ScrollLock" on iOS for now. + } else { + if (HardwareKeyboard.instance.lockModesEnabled + .contains(KeyboardLockMode.capsLock)) { + lockModes |= (1 << capslock); + } + if (HardwareKeyboard.instance.lockModesEnabled + .contains(KeyboardLockMode.numLock)) { + lockModes |= (1 << numlock); + } + if (HardwareKeyboard.instance.lockModesEnabled + .contains(KeyboardLockMode.scrollLock)) { + lockModes |= (1 << scrolllock); + } + } + return lockModes; + } + // This function must be called after the peer info is received. // Because `sessionGetKeyboardMode` relies on the peer version. updateKeyboardMode() async { @@ -550,6 +619,11 @@ class InputModel { return KeyEventResult.handled; } + bool iosCapsLock = false; + if (isIOS && e is RawKeyDownEvent) { + iosCapsLock = _getIosCapsFromRawCharacter(e); + } + final key = e.logicalKey; if (e is RawKeyDownEvent) { if (!e.repeat) { @@ -586,7 +660,7 @@ class InputModel { // * Currently mobile does not enable map mode if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) { - mapKeyboardModeRaw(e); + mapKeyboardModeRaw(e, iosCapsLock); } else { legacyKeyboardModeRaw(e); } @@ -622,6 +696,11 @@ class InputModel { return KeyEventResult.handled; } + bool iosCapsLock = false; + if (isIOS && (e is KeyDownEvent || e is KeyRepeatEvent)) { + iosCapsLock = _getIosCapsFromCharacter(e); + } + if (e is KeyUpEvent) { handleKeyUpEventModifiers(e); } else if (e is KeyDownEvent) { @@ -667,7 +746,8 @@ class InputModel { e.character ?? '', e.physicalKey.usbHidUsage & 0xFFFF, // Show repeat event be converted to "release+press" events? - e is KeyDownEvent || e is KeyRepeatEvent); + e is KeyDownEvent || e is KeyRepeatEvent, + iosCapsLock); } else { legacyKeyboardMode(e); } @@ -676,23 +756,9 @@ class InputModel { } /// Send Key Event - void newKeyboardMode(String character, int usbHid, bool down) { - const capslock = 1; - const numlock = 2; - const scrolllock = 3; - int lockModes = 0; - if (HardwareKeyboard.instance.lockModesEnabled - .contains(KeyboardLockMode.capsLock)) { - lockModes |= (1 << capslock); - } - if (HardwareKeyboard.instance.lockModesEnabled - .contains(KeyboardLockMode.numLock)) { - lockModes |= (1 << numlock); - } - if (HardwareKeyboard.instance.lockModesEnabled - .contains(KeyboardLockMode.scrollLock)) { - lockModes |= (1 << scrolllock); - } + void newKeyboardMode( + String character, int usbHid, bool down, bool iosCapsLock) { + final lockModes = _buildLockModes(iosCapsLock); bind.sessionHandleFlutterKeyEvent( sessionId: sessionId, character: character, @@ -701,7 +767,7 @@ class InputModel { downOrUp: down); } - void mapKeyboardModeRaw(RawKeyEvent e) { + void mapKeyboardModeRaw(RawKeyEvent e, bool iosCapsLock) { int positionCode = -1; int platformCode = -1; bool down; @@ -732,27 +798,14 @@ class InputModel { } else { down = false; } - inputRawKey(e.character ?? '', platformCode, positionCode, down); + inputRawKey( + e.character ?? '', platformCode, positionCode, down, iosCapsLock); } /// Send raw Key Event - void inputRawKey(String name, int platformCode, int positionCode, bool down) { - const capslock = 1; - const numlock = 2; - const scrolllock = 3; - int lockModes = 0; - if (HardwareKeyboard.instance.lockModesEnabled - .contains(KeyboardLockMode.capsLock)) { - lockModes |= (1 << capslock); - } - if (HardwareKeyboard.instance.lockModesEnabled - .contains(KeyboardLockMode.numLock)) { - lockModes |= (1 << numlock); - } - if (HardwareKeyboard.instance.lockModesEnabled - .contains(KeyboardLockMode.scrollLock)) { - lockModes |= (1 << scrolllock); - } + void inputRawKey(String name, int platformCode, int positionCode, bool down, + bool iosCapsLock) { + final lockModes = _buildLockModes(iosCapsLock); bind.sessionHandleFlutterRawKeyEvent( sessionId: sessionId, name: name, @@ -1800,9 +1853,9 @@ class InputModel { // Simulate a key press event. // `usbHidUsage` is the USB HID usage code of the key. Future tapHidKey(int usbHidUsage) async { - newKeyboardMode(kKeyFlutterKey, usbHidUsage, true); + newKeyboardMode(kKeyFlutterKey, usbHidUsage, true, false); await Future.delayed(Duration(milliseconds: 100)); - newKeyboardMode(kKeyFlutterKey, usbHidUsage, false); + newKeyboardMode(kKeyFlutterKey, usbHidUsage, false, false); } Future onMobileVolumeUp() async => From 8c6dcf53a6bcf8eb95536ee1a86d222b9167903a Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Jan 2026 16:37:45 +0800 Subject: [PATCH 116/277] iOS terminal: Add touch swipe and floating back button for exit (#14208) * Initial plan * Add iOS edge swipe gesture to exit terminal session Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com> * Improve iOS edge swipe gesture with responsive thresholds and better gesture handling Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com> * Fix: Reset _swipeCurrentX in onHorizontalDragStart to prevent stale state Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com> * Add trackpad support documentation for iOS edge swipe gesture Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com> * Add iOS-style circular back button to terminal page Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com> * Remove trackpad support documentation - not needed with back button Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com> * Filter edge swipe gesture to touch-only input (exclude mouse/trackpad) Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com> * fix: missing import Signed-off-by: fufesou * fix(ios): terminal swip exit gesture Signed-off-by: fufesou * Update flutter/lib/mobile/pages/terminal_page.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Signed-off-by: fufesou Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com> Co-authored-by: fufesou Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- flutter/lib/mobile/pages/terminal_page.dart | 105 +++++++++++++++++++- 1 file changed, 104 insertions(+), 1 deletion(-) diff --git a/flutter/lib/mobile/pages/terminal_page.dart b/flutter/lib/mobile/pages/terminal_page.dart index 67d77782f..ab34a35ec 100644 --- a/flutter/lib/mobile/pages/terminal_page.dart +++ b/flutter/lib/mobile/pages/terminal_page.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:math'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; @@ -41,6 +42,9 @@ class _TerminalPageState extends State final GlobalKey _keyboardKey = GlobalKey(); double _keyboardHeight = 0; late bool _showTerminalExtraKeys; + // For iOS edge swipe gesture + double _swipeStartX = 0; + double _swipeCurrentX = 0; // For web only. // 'monospace' does not work on web, use Google Fonts, `??` is only for null safety. @@ -147,7 +151,7 @@ class _TerminalPageState extends State } Widget buildBody() { - return Scaffold( + final scaffold = Scaffold( resizeToAvoidBottomInset: false, // Disable automatic layout adjustment; manually control UI updates to prevent flickering when the keyboard shows/hides backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: Stack( @@ -192,9 +196,108 @@ class _TerminalPageState extends State ), ), if (_showTerminalExtraKeys) _buildFloatingKeyboard(), + // iOS-style circular close button in top-right corner + if (isIOS) _buildCloseButton(), ], ), ); + + // Add iOS edge swipe gesture to exit (similar to Android back button) + if (isIOS) { + return LayoutBuilder( + builder: (context, constraints) { + final screenWidth = constraints.maxWidth; + // Base thresholds on screen width but clamp to reasonable logical pixel ranges + // Edge detection region: ~10% of width, clamped between 20 and 80 logical pixels + final edgeThreshold = (screenWidth * 0.1).clamp(20.0, 80.0); + // Required horizontal movement: ~25% of width, clamped between 80 and 300 logical pixels + final swipeThreshold = (screenWidth * 0.25).clamp(80.0, 300.0); + + return RawGestureDetector( + behavior: HitTestBehavior.translucent, + gestures: { + HorizontalDragGestureRecognizer: GestureRecognizerFactoryWithHandlers( + () => HorizontalDragGestureRecognizer( + debugOwner: this, + // Only respond to touch input, exclude mouse/trackpad + supportedDevices: kTouchBasedDeviceKinds, + ), + (HorizontalDragGestureRecognizer instance) { + instance + // Capture initial touch-down position (before touch slop) + ..onDown = (details) { + _swipeStartX = details.localPosition.dx; + _swipeCurrentX = details.localPosition.dx; + } + ..onUpdate = (details) { + _swipeCurrentX = details.localPosition.dx; + } + ..onEnd = (details) { + // Check if swipe started from left edge and moved right + if (_swipeStartX < edgeThreshold && (_swipeCurrentX - _swipeStartX) > swipeThreshold) { + clientClose(sessionId, _ffi); + } + _swipeStartX = 0; + _swipeCurrentX = 0; + } + ..onCancel = () { + _swipeStartX = 0; + _swipeCurrentX = 0; + }; + }, + ), + }, + child: scaffold, + ); + }, + ); + } + + return scaffold; + } + + Widget _buildCloseButton() { + return Positioned( + top: 0, + right: 0, + child: SafeArea( + minimum: const EdgeInsets.only( + top: 16, // iOS standard margin + right: 16, // iOS standard margin + ), + child: Semantics( + button: true, + label: translate('Close'), + child: Container( + width: 44, // iOS standard tap target size + height: 44, + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), // Half transparency + shape: BoxShape.circle, + ), + child: Material( + color: Colors.transparent, + shape: const CircleBorder(), + clipBehavior: Clip.antiAlias, + child: InkWell( + customBorder: const CircleBorder(), + onTap: () { + clientClose(sessionId, _ffi); + }, + child: Tooltip( + message: translate('Close'), + child: const Icon( + Icons.chevron_left, // iOS-style back arrow + color: Colors.white, + size: 28, + ), + ), + ), + ), + ), + ), + ), + ); } Widget _buildFloatingKeyboard() { From 96075fdf4969cf19f619f75e11b5a5a14c02aafa Mon Sep 17 00:00:00 2001 From: XLion Date: Sat, 31 Jan 2026 16:38:09 +0800 Subject: [PATCH 117/277] Update tw.rs (#14138) --- src/lang/tw.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 6bde1e7c8..c4067feec 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -729,15 +729,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", "注意:RustDesk 開源伺服器 (OSS server) 不包含此功能。"), ("input note here", "輸入備註"), ("note-at-conn-end-tip", "在連接結束時請求備註"), - ("Show terminal extra keys", ""), - ("Relative mouse mode", ""), - ("rel-mouse-not-supported-peer-tip", ""), - ("rel-mouse-not-ready-tip", ""), - ("rel-mouse-lock-failed-tip", ""), - ("rel-mouse-exit-{}-tip", ""), - ("rel-mouse-permission-lost-tip", ""), - ("Changelog", ""), - ("keep-awake-during-outgoing-sessions-label", ""), - ("keep-awake-during-incoming-sessions-label", ""), + ("Show terminal extra keys", "顯示終端機額外按鍵"), + ("Relative mouse mode", "相對滑鼠模式"), + ("rel-mouse-not-supported-peer-tip", "被控端不支援相對滑鼠模式"), + ("rel-mouse-not-ready-tip", "相對滑鼠模式尚未就緒,請稍候再試"), + ("rel-mouse-lock-failed-tip", "無法鎖定游標,相對滑鼠模式已停用"), + ("rel-mouse-exit-{}-tip", "按下 {} 退出"), + ("rel-mouse-permission-lost-tip", "鍵盤權限被撤銷,相對滑鼠模式已被停用"), + ("Changelog", "更新日誌"), + ("keep-awake-during-outgoing-sessions-label", "在連出工作階段期間保持螢幕喚醒"), + ("keep-awake-during-incoming-sessions-label", "在連入工作階段期間保持螢幕喚醒"), ].iter().cloned().collect(); } From 6306f833163c083bafae874a74410343240a89b6 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 12:18:07 +0800 Subject: [PATCH 118/277] Fix non-link text color in dialogs with links for dark theme (#14220) * Initial plan * Fix dialog text color for dark theme with links Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com> * Keep original link color (blue), only fix non-link text color Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com> * fix: dialog text color in dark theme Signed-off-by: fufesou --------- Signed-off-by: fufesou Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com> Co-authored-by: fufesou --- flutter/lib/common.dart | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 0650b1b5b..b941632dd 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1124,18 +1124,23 @@ class CustomAlertDialog extends StatelessWidget { Widget createDialogContent(String text) { final RegExp linkRegExp = RegExp(r'(https?://[^\s]+)'); + bool hasLink = linkRegExp.hasMatch(text); + + // Early return: no link, use default theme color + if (!hasLink) { + return SelectableText(text, style: const TextStyle(fontSize: 15)); + } + final List spans = []; int start = 0; - bool hasLink = false; linkRegExp.allMatches(text).forEach((match) { - hasLink = true; if (match.start > start) { spans.add(TextSpan(text: text.substring(start, match.start))); } spans.add(TextSpan( text: match.group(0) ?? '', - style: TextStyle( + style: const TextStyle( color: Colors.blue, decoration: TextDecoration.underline, ), @@ -1153,13 +1158,9 @@ Widget createDialogContent(String text) { spans.add(TextSpan(text: text.substring(start))); } - if (!hasLink) { - return SelectableText(text, style: const TextStyle(fontSize: 15)); - } - return SelectableText.rich( TextSpan( - style: TextStyle(color: Colors.black, fontSize: 15), + style: const TextStyle(fontSize: 15), children: spans, ), ); From 5ee9dcf42d6944c58722982bd36278d3bdb5892a Mon Sep 17 00:00:00 2001 From: bilimiyorum <131397022+bilimiyorum@users.noreply.github.com> Date: Mon, 2 Feb 2026 17:18:36 +0300 Subject: [PATCH 119/277] Update tr.rs (#14160) The previous PR was reverted due to an incorrect file path. This PR applies the same updates to src/lang/tr.rs. --- src/lang/tr.rs | 132 ++++++++++++++++++++++++------------------------- 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/src/lang/tr.rs b/src/lang/tr.rs index fdb5d0322..08f8de37f 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -3,8 +3,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Durum"), ("Your Desktop", "Sizin Masaüstünüz"), - ("desk_tip", "Masaüstünüze bu ID ve şifre ile erişilebilir"), - ("Password", "Şifre"), + ("desk_tip", "Masaüstünüze bu ID ve parola ile erişilebilir"), + ("Password", "Parola"), ("Ready", "Hazır"), ("Established", "Bağlantı sağlandı"), ("connecting_status", "Bağlanılıyor "), @@ -13,16 +13,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Service is running", "Servis çalışıyor"), ("Service is not running", "Servis çalışmıyor"), ("not_ready_status", "Hazır değil. Bağlantınızı kontrol edin"), - ("Control Remote Desktop", "Bağlanılacak Uzak Bağlantı ID"), + ("Control Remote Desktop", "Uzak Masaüstünü Denetle"), ("Transfer file", "Dosya transferi"), ("Connect", "Bağlan"), - ("Recent sessions", "Son Bağlanılanlar"), + ("Recent sessions", "Son oturumlar"), ("Address book", "Adres Defteri"), ("Confirmation", "Onayla"), - ("TCP tunneling", "TCP Tünelleri"), + ("TCP tunneling", "TCP tünelleri"), ("Remove", "Kaldır"), - ("Refresh random password", "Yeni rastgele şifre oluştur"), - ("Set your own password", "Kendi şifreni oluştur"), + ("Refresh random password", "Yeni rastgele parola oluştur"), + ("Set your own password", "Kendi parolanı oluştur"), ("Enable keyboard/mouse", "Klavye ve Fareye izin ver"), ("Enable clipboard", "Kopyalanan geçici veriye izin ver"), ("Enable file transfer", "Dosya Transferine izin ver"), @@ -47,9 +47,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Slogan_tip", "Bu kaotik dünyada gönülden yapıldı!"), ("Privacy Statement", "Gizlilik Beyanı"), ("Mute", "Sustur"), - ("Build Date", "Yapım Tarihi"), + ("Build Date", "Derleme Tarihi"), ("Version", "Sürüm"), - ("Home", "Anasayfa"), + ("Home", "Ana Sayfa"), ("Audio Input", "Ses Girişi"), ("Enhancements", "Geliştirmeler"), ("Hardware Codec", "Donanımsal Codec"), @@ -64,18 +64,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Not available", "Erişilebilir değil"), ("Too frequent", "Çok sık"), ("Cancel", "İptal"), - ("Skip", "Geç"), + ("Skip", "Atla"), ("Close", "Kapat"), ("Retry", "Tekrar Dene"), ("OK", "Tamam"), - ("Password Required", "Şifre Gerekli"), - ("Please enter your password", "Lütfen şifrenizi giriniz"), - ("Remember password", "Şifreyi hatırla"), - ("Wrong Password", "Hatalı şifre"), + ("Password Required", "Parola Gerekli"), + ("Please enter your password", "Lütfen parolanızı giriniz"), + ("Remember password", "Parolayı hatırla"), + ("Wrong Password", "Hatalı parola"), ("Do you want to enter again?", "Tekrar giriş yapmak ister misiniz?"), ("Connection Error", "Bağlantı Hatası"), ("Error", "Hata"), - ("Reset by the peer", "Eş tarafında sıfırla"), + ("Reset by the peer", "Eş tarafından sıfırlandı"), ("Connecting...", "Bağlanılıyor..."), ("Connection in progress. Please wait.", "Bağlantı sağlanıyor. Lütfen bekleyiniz."), ("Please try 1 minute later", "Lütfen 1 dakika sonra tekrar deneyiniz"), @@ -141,10 +141,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Timeout", "Zaman aşımı"), ("Failed to connect to relay server", "Relay sunucusuna bağlanılamadı"), ("Failed to connect via rendezvous server", "ID oluşturma sunucusuna bağlanılamadı"), - ("Failed to connect via relay server", "Relay oluşturma sunucusuna bağlanılamadı"), + ("Failed to connect via relay server", "Aktarma sunucusuna bağlanılamadı"), ("Failed to make direct connection to remote desktop", "Uzak masaüstüne doğrudan bağlantı kurulamadı"), - ("Set Password", "Şifre ayarla"), - ("OS Password", "İşletim Sistemi Şifresi"), + ("Set Password", "Parola ayarla"), + ("OS Password", "İşletim Sistemi Parolası"), ("install_tip", "Kullanıcı Hesabı Denetimi nedeniyle, RustDesk bir uzak masaüstü olarak düzgün çalışmayabilir. Bu sorunu önlemek için, RustDesk'i sistem seviyesinde kurmak için aşağıdaki butona tıklayın."), ("Click to upgrade", "Yükseltmek için tıklayınız"), ("Configure", "Ayarla"), @@ -184,7 +184,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Direct and unencrypted connection", "Doğrudan ve şifrelenmemiş bağlantı"), ("Relayed and unencrypted connection", "Aktarmalı ve şifrelenmemiş bağlantı"), ("Enter Remote ID", "Uzak ID'yi Girin"), - ("Enter your password", "Şifrenizi girin"), + ("Enter your password", "Parolanızı girin"), ("Logging in...", "Giriş yapılıyor..."), ("Enable RDP session sharing", "RDP oturum paylaşımını etkinleştir"), ("Auto Login", "Otomatik giriş"), @@ -208,8 +208,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Eş tarafından manuel olarak kapatıldı"), ("Enable remote configuration modification", "Uzaktan yapılandırma değişikliğini etkinleştir"), ("Run without install", "Yüklemeden çalıştır"), - ("Connect via relay", ""), - ("Always connect via relay", "Always connect via relay"), + ("Connect via relay", "Aktarmalı üzerinden bağlan"), + ("Always connect via relay", "Her zaman aktarmalı üzerinden bağlan"), ("whitelist_tip", "Bu masaüstüne yalnızca yetkili IP adresleri bağlanabilir"), ("Login", "Giriş yap"), ("Verify", "Doğrula"), @@ -226,11 +226,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unselect all tags", "Tüm etiketlerin seçimini kaldır"), ("Network error", "Bağlantı hatası"), ("Username missed", "Kullanıcı adı boş"), - ("Password missed", "Şifre boş"), + ("Password missed", "Parola boş"), ("Wrong credentials", "Yanlış kimlik bilgileri"), ("The verification code is incorrect or has expired", "Doğrulama kodu hatalı veya süresi dolmuş"), ("Edit Tag", "Etiketi düzenle"), - ("Forget Password", "Şifreyi Unut"), + ("Forget Password", "Parolayı Unut"), ("Favorites", "Favoriler"), ("Add to Favorites", "Favorilere ekle"), ("Remove from Favorites", "Favorilerden çıkar"), @@ -268,9 +268,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Share screen", "Ekranı Paylaş"), ("Chat", "Mesajlaş"), ("Total", "Toplam"), - ("items", "öğeler"), + ("items", "ögeler"), ("Selected", "Seçildi"), - ("Screen Capture", "Ekran görüntüsü"), + ("Screen Capture", "Ekran Görüntüsü"), ("Input Control", "Giriş Kontrolü"), ("Audio Capture", "Ses Yakalama"), ("Do you accept?", "Kabul ediyor musun?"), @@ -285,7 +285,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_start_service_tip", "Ekran paylaşım hizmetini başlatmak için [Hizmeti başlat] ögesine dokunun veya [Ekran Görüntüsü] iznini etkinleştirin."), ("android_permission_may_not_change_tip", "Kurulan bağlantılara ait izinler, yeniden bağlantı kurulana kadar anında değiştirilemez."), ("Account", "Hesap"), - ("Overwrite", "üzerine yaz"), + ("Overwrite", "Üzerine yaz"), ("This file exists, skip or overwrite this file?", "Bu dosya var, bu dosya atlansın veya üzerine yazılsın mı?"), ("Quit", "Çıkış"), ("Help", "Yardım"), @@ -295,8 +295,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unsupported", "desteklenmiyor"), ("Peer denied", "eş reddedildi"), ("Please install plugins", "Lütfen eklentileri yükleyin"), - ("Peer exit", "eş çıkışı"), - ("Failed to turn off", "kapatılamadı"), + ("Peer exit", "Eş çıkışı"), + ("Failed to turn off", "Kapatılamadı"), ("Turned off", "Kapatıldı"), ("Language", "Dil"), ("Keep RustDesk background service", "RustDesk arka plan hizmetini sürdürün"), @@ -308,32 +308,32 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Legacy mode", "Eski mod"), ("Map mode", "Haritalama modu"), ("Translate mode", "Çeviri modu"), - ("Use permanent password", "Kalıcı şifre kullan"), - ("Use both passwords", "İki şifreyi de kullan"), - ("Set permanent password", "Kalıcı şifre oluştur"), + ("Use permanent password", "Kalıcı parola kullan"), + ("Use both passwords", "İki parolayı da kullan"), + ("Set permanent password", "Kalıcı parola oluştur"), ("Enable remote restart", "Uzaktan yeniden başlatmayı aktif et"), ("Restart remote device", "Uzaktaki cihazı yeniden başlat"), - ("Are you sure you want to restart", "Yeniden başlatmak istediğinize emin misin?"), + ("Are you sure you want to restart", "Yeniden başlatmak istediğine emin misin?"), ("Restarting remote device", "Uzaktan yeniden başlatılıyor"), - ("remote_restarting_tip", "Uzak cihaz yeniden başlatılıyor, lütfen bu mesaj kutusunu kapatın ve bir süre sonra kalıcı şifre ile yeniden bağlanın"), + ("remote_restarting_tip", "Uzak cihaz yeniden başlatılıyor, lütfen bu mesaj kutusunu kapatın ve bir süre sonra kalıcı parola ile yeniden bağlanın"), ("Copied", "Kopyalandı"), - ("Exit Fullscreen", "Tam ekrandan çık"), - ("Fullscreen", "Tam ekran"), + ("Exit Fullscreen", "Tam Ekrandan Çık"), + ("Fullscreen", "Tam Ekran"), ("Mobile Actions", "Mobil İşlemler"), ("Select Monitor", "Monitörü Seç"), ("Control Actions", "Kontrol Eylemleri"), - ("Display Settings", "Görüntü ayarları"), + ("Display Settings", "Görüntü Ayarları"), ("Ratio", "Oran"), - ("Image Quality", "Görüntü kalitesi"), + ("Image Quality", "Görüntü Kalitesi"), ("Scroll Style", "Kaydırma Stili"), ("Show Toolbar", "Araç Çubuğunu Göster"), ("Hide Toolbar", "Araç Çubuğunu Gizle"), ("Direct Connection", "Doğrudan Bağlantı"), - ("Relay Connection", "Röle Bağlantısı"), + ("Relay Connection", "Aktarmalı Bağlantı"), ("Secure Connection", "Güvenli Bağlantı"), ("Insecure Connection", "Güvenli Olmayan Bağlantı"), - ("Scale original", "Orijinali ölçeklendir"), - ("Scale adaptive", "Ölçek uyarlanabilir"), + ("Scale original", "Orijinal ölçekte"), + ("Scale adaptive", "Uyarlanabilir ölçekte"), ("General", "Genel"), ("Security", "Güvenlik"), ("Theme", "Tema"), @@ -347,18 +347,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable audio", "Sesi Aktif Et"), ("Unlock Network Settings", "Ağ Ayarlarını Aç"), ("Server", "Sunucu"), - ("Direct IP Access", "Direk IP Erişimi"), + ("Direct IP Access", "Doğrudan IP Erişimi"), ("Proxy", "Vekil"), ("Apply", "Uygula"), - ("Disconnect all devices?", "Tüm cihazların bağlantısını kes?"), + ("Disconnect all devices?", "Tüm cihazların bağlantısı kesilsin mi?"), ("Clear", "Temizle"), ("Audio Input Device", "Ses Giriş Aygıtı"), ("Use IP Whitelisting", "IP Beyaz Listeyi Kullan"), ("Network", "Ağ"), ("Pin Toolbar", "Araç Çubuğunu Sabitle"), ("Unpin Toolbar", "Araç Çubuğunu Sabitlemeyi Kaldır"), - ("Recording", "Kayıt Ediliyor"), - ("Directory", "Klasör"), + ("Recording", "Kaydediliyor"), + ("Directory", "Dizin"), ("Automatically record incoming sessions", "Gelen oturumları otomatik olarak kaydet"), ("Automatically record outgoing sessions", "Giden oturumları otomatik olarak kaydet"), ("Change", "Değiştir"), @@ -384,16 +384,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "RustDesk'i Göster"), ("This PC", "Bu PC"), ("or", "veya"), - ("Continue with", "bununla devam et"), + ("Continue with", "Bununla devam et"), ("Elevate", "Yükseltme"), ("Zoom cursor", "Yakınlaştırma imleci"), ("Accept sessions via password", "Oturumları parola ile kabul etme"), ("Accept sessions via click", "Tıklama yoluyla oturumları kabul edin"), ("Accept sessions via both", "Her ikisi aracılığıyla oturumları kabul edin"), ("Please wait for the remote side to accept your session request...", "Lütfen uzak tarafın oturum isteğinizi kabul etmesini bekleyin..."), - ("One-time Password", "Tek Kullanımlık Şifre"), + ("One-time Password", "Tek Kullanımlık Parola"), ("Use one-time password", "Tek seferlik parola kullanın"), - ("One-time password length", "Tek seferlik şifre uzunluğu"), + ("One-time password length", "Tek seferlik parola uzunluğu"), ("Request access to your device", "Cihazınıza erişim talep edin"), ("Hide connection management window", "Bağlantı yönetimi penceresini gizle"), ("hide_cm_tip", "Oturumları yalnızca parola ile kabul edebilir ve kalıcı parola kullanıyorsanız gizlemeye izin verin"), @@ -442,7 +442,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Sesli görüşme"), ("Text chat", "Metin sohbeti"), ("Stop voice call", "Sesli görüşmeyi durdur"), - ("relay_hint_tip", "Doğrudan bağlanmak mümkün olmayabilir; röle aracılığıyla bağlanmayı deneyebilirsiniz. Ayrıca, ilk denemenizde bir röle kullanmak istiyorsanız, ID'nin sonuna \"/r\" ekleyebilir veya son oturum kartındaki \"Her Zaman Röle Üzerinden Bağlan\" seçeneğini seçebilirsiniz."), + ("relay_hint_tip", "Doğrudan bağlanmak mümkün olmayabilir; aktarmalı bağlanmayı deneyebilirsiniz. Ayrıca, ilk denemenizde aktarma sunucusu kullanmak istiyorsanız ID'nin sonuna \"/r\" ekleyebilir veya son oturum kartındaki \"Her Zaman Aktarmalı Üzerinden Bağlan\" seçeneğini seçebilirsiniz."), ("Reconnect", "Yeniden Bağlan"), ("Codec", "Kodlayıcı"), ("Resolution", "Çözünürlük"), @@ -477,7 +477,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("no_desktop_title_tip", "Masaüstü mevcut değil"), ("no_desktop_text_tip", "Lütfen GNOME masaüstünü yükleyin"), ("No need to elevate", "Yükseltmeye gerek yok"), - ("System Sound", "Sistem Ses"), + ("System Sound", "Sistem Sesi"), ("Default", "Varsayılan"), ("New RDP", "Yeni RDP"), ("Fingerprint", "Parmak İzi"), @@ -495,7 +495,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("resolution_fit_local_tip", "Yerel çözünürlüğe sığdır"), ("resolution_custom_tip", "Özel çözünürlük"), ("Collapse toolbar", "Araç çubuğunu daralt"), - ("Accept and Elevate", "Kabul et ve yükselt"), + ("Accept and Elevate", "Kabul Et ve Yükselt"), ("accept_and_elevate_btn_tooltip", "Bağlantıyı kabul et ve UAC izinlerini yükselt."), ("clipboard_wait_response_timeout_tip", "Kopyalama yanıtı için zaman aşımına uğradı."), ("Incoming connection", "Gelen bağlantı"), @@ -534,7 +534,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("scam_text1", "Eğer tanımadığınız ve güvenmediğiniz birisiyle telefonda konuşuyorsanız ve sizden RustDesk'i kullanmanızı ve hizmeti başlatmanızı istiyorsa devam etmeyin ve hemen telefonu kapatın."), ("scam_text2", "Muhtemelen paranızı veya diğer özel bilgilerinizi çalmaya çalışan dolandırıcılardır."), ("Don't show again", "Bir daha gösterme"), - ("I Agree", "Kabul ediyorum"), + ("I Agree", "Kabul Ediyorum"), ("Decline", "Reddet"), ("Timeout in minutes", "Zaman aşımı (dakika)"), ("auto_disconnect_option_tip", "Kullanıcı etkin olmadığında gelen oturumları otomatik olarak kapat"), @@ -559,7 +559,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Plug out all", "Tümünü çıkar"), ("True color (4:4:4)", "Gerçek renk (4:4:4)"), ("Enable blocking user input", "Kullanıcı girişini engellemeyi etkinleştir"), - ("id_input_tip", "Bir ID, doğrudan IP veya portlu bir etki alanı (:) girebilirsiniz.\nBaşka bir sunucudaki bir cihaza erişmek istiyorsanız lütfen sunucu adresini (@?key=) ekleyin, örneğin,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nGenel bir sunucudaki bir cihaza erişmek istiyorsanız lütfen \"@public\" girin, genel sunucu için anahtara gerek yoktur.\n\nİlk bağlantıda bir röle bağlantısının kullanılmasını zorlamak istiyorsanız ID'nin sonuna \"/r\" ekleyin, örneğin, \"9123456234/r\"."), + ("id_input_tip", "Bir ID, doğrudan IP veya portlu bir etki alanı (:) girebilirsiniz.\nBaşka bir sunucudaki bir cihaza erişmek istiyorsanız lütfen sunucu adresini (@?key=) ekleyin, örneğin,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nGenel bir sunucudaki bir cihaza erişmek istiyorsanız lütfen \"@public\" girin, genel sunucu için anahtara gerek yoktur.\n\nİlk bağlantıda bir aktarma bağlantısının kullanılmasını zorlamak istiyorsanız ID'nin sonuna \"/r\" ekleyin, örneğin, \"9123456234/r\"."), ("privacy_mode_impl_mag_tip", "Mod 1"), ("privacy_mode_impl_virtual_display_tip", "Mod 2"), ("Enter privacy mode", "Gizlilik moduna gir"), @@ -581,12 +581,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please select the session you want to connect to", "Lütfen bağlanmak istediğiniz oturumu seçin"), ("powered_by_me", "RustDesk tarafından desteklenmektedir"), ("outgoing_only_desk_tip", "Bu özelleştirilmiş bir sürümdür.\nDiğer cihazlara bağlanabilirsiniz, ancak diğer cihazlar cihazınıza bağlanamaz."), - ("preset_password_warning", "Bu özelleştirilmiş sürüm, önceden ayarlanmış bir şifre ile birlikte gelir. Bu parolayı bilen herkes cihazınızın tam kontrolünü ele geçirebilir. Bunu beklemiyorsanız yazılımı hemen kaldırın."), + ("preset_password_warning", "Bu özelleştirilmiş sürüm, önceden ayarlanmış bir parola ile birlikte gelir. Bu parolayı bilen herkes cihazınızın tam kontrolünü ele geçirebilir. Bunu beklemiyorsanız yazılımı hemen kaldırın."), ("Security Alert", "Güvenlik Uyarısı"), ("My address book", "Adres defterim"), ("Personal", "Kişisel"), ("Owner", "Sahip"), - ("Set shared password", "Paylaşılan şifreyi ayarla"), + ("Set shared password", "Paylaşılan parolayı ayarla"), ("Exist in", "İçinde varolan"), ("Read-only", "Salt okunur"), ("Read/Write", "Okuma/Yazma"), @@ -599,7 +599,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Follow remote cursor", "Uzak imleci takip et"), ("Follow remote window focus", "Uzak pencere odağını takip et"), ("default_proxy_tip", "Varsayılan protokol ve port Socks5 ve 1080'dir."), - ("no_audio_input_device_tip", "Varsayılan protokol ve port, Socks5 ve 1080'dir"), + ("no_audio_input_device_tip", "Ses girişi aygıtı bulunamadı."), ("Incoming", "Gelen"), ("Outgoing", "Giden"), ("Clear Wayland screen selection", "Wayland ekran seçimini temizle"), @@ -612,7 +612,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("floating_window_tip", "RustDesk arka plan hizmetini açık tutmaya yardımcı olur"), ("Keep screen on", "Ekranı açık tut"), ("Never", "Asla"), - ("During controlled", "Kontrol sırasınd"), + ("During controlled", "Kontrol sırasında"), ("During service is on", "Servis açıkken"), ("Capture screen using DirectX", "DirectX kullanarak ekran görüntüsü al"), ("Back", "Geri"), @@ -620,7 +620,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Volume up", "Sesi yükselt"), ("Volume down", "Sesi azalt"), ("Power", "Güç"), - ("Telegram bot", "Telegram bot"), + ("Telegram bot", "Telegram botu"), ("enable-bot-tip", "Bu özelliği etkinleştirirseniz botunuzdan 2FA kodunu alabilirsiniz. Aynı zamanda bağlantı bildirimi işlevi de görebilir."), ("enable-bot-desc", "1. @BotFather ile bir sohbet açın.\n2. \"/newbot\" komutunu gönderin. Bu adımı tamamladıktan sonra bir jeton alacaksınız.\n3. Yeni oluşturduğunuz botla bir sohbet başlatın. Etkinleştirmek için eğik çizgiyle (\"/\") başlayan \"/merhaba\" gibi bir mesaj gönderin.\n"), ("cancel-2fa-confirm-tip", "2FA'yı iptal etmek istediğinizden emin misiniz?"), @@ -642,7 +642,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Invalid file name", "Geçersiz dosya adı"), ("one-way-file-transfer-tip", "Kontrol edilen tarafta tek yönlü dosya transferi aktiftir."), ("Authentication Required", "Kimlik Doğrulama Gerekli"), - ("Authenticate", "Kimlik doğrulaması"), + ("Authenticate", "Kimlik Doğrula"), ("web_id_input_tip", "Aynı sunucuda bir kimlik girebilirsiniz, web istemcisinde doğrudan IP erişimi desteklenmez.\nBaşka bir sunucudaki bir cihaza erişmek istiyorsanız lütfen sunucu adresini (@?key=) ekleyin, örneğin,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nGenel bir sunucudaki bir cihaza erişmek istiyorsanız, lütfen \"@public\" girin, genel sunucu için anahtara gerek yoktur."), ("Download", "İndir"), ("Upload folder", "Klasör yükle"), @@ -661,9 +661,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-not-installed-tip", "{} Yazıcısı yüklü değil."), ("printer-{}-ready-tip", "{} Yazıcısı kuruldu ve kullanıma hazır."), ("Install {} Printer", "{} Yazıcısını Yükle"), - ("Outgoing Print Jobs", "Giden Baskı İşleri"), - ("Incoming Print Jobs", "Gelen Baskı İşleri"), - ("Incoming Print Job", "Gelen Baskı İşi"), + ("Outgoing Print Jobs", "Giden Yazdırma İşleri"), + ("Incoming Print Jobs", "Gelen Yazdırma İşleri"), + ("Incoming Print Job", "Gelen Yazdırma İşi"), ("use-the-default-printer-tip", "Varsayılan yazıcıyı kullan"), ("use-the-selected-printer-tip", "Seçili yazıcıyı kullan"), ("auto-print-tip", "Seçili yazıcıyı kullanarak otomatik olarak yazdır."), @@ -685,11 +685,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("download-new-version-failed-tip", "İndirme başarısız oldu. Tekrar deneyebilir veya 'İndir' düğmesine tıklayarak sürüm sayfasından manuel olarak indirip güncelleyebilirsiniz."), ("Auto update", "Otomatik güncelleme"), ("update-failed-check-msi-tip", "Kurulum yöntemi denetimi başarısız oldu. Sürüm sayfasından indirmek ve manuel olarak yükseltmek için lütfen \"İndir\" düğmesine tıklayın."), - ("websocket_tip", "WebSocket kullanıldığında yalnızca röle bağlantıları desteklenir."), + ("websocket_tip", "WebSocket kullanıldığında yalnızca aktarma bağlantıları desteklenir."), ("Use WebSocket", "WebSocket'ı kullan"), ("Trackpad speed", "İzleme paneli hızı"), ("Default trackpad speed", "Varsayılan izleme paneli hızı"), - ("Numeric one-time password", "Sayısal tek seferlik şifre"), + ("Numeric one-time password", "Sayısal tek seferlik parola"), ("Enable IPv6 P2P connection", "IPv6 P2P bağlantısını etkinleştir"), ("Enable UDP hole punching", "UDP delik açmayı etkinleştir"), ("View camera", "Kamerayı görüntüle"), @@ -701,16 +701,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("New tab", "Yeni sekme"), ("Keep terminal sessions on disconnect", "Bağlantı kesildiğinde terminal oturumlarını açık tut"), ("Terminal (Run as administrator)", "Terminal (Yönetici olarak çalıştır)"), - ("terminal-admin-login-tip", "Lütfen kontrol edilen tarafın yönetici kullanıcı adı ve şifresini giriniz."), + ("terminal-admin-login-tip", "Lütfen kontrol edilen tarafın yönetici kullanıcı adı ve parolasını giriniz."), ("Failed to get user token.", "Kullanıcı belirteci alınamadı."), - ("Incorrect username or password.", "Hatalı kullanıcı adı veya şifre."), + ("Incorrect username or password.", "Hatalı kullanıcı adı veya parola."), ("The user is not an administrator.", "Kullanıcı bir yönetici değil."), ("Failed to check if the user is an administrator.", "Kullanıcının yönetici olup olmadığı kontrol edilemedi."), ("Supported only in the installed version.", "Sadece yüklü sürümde desteklenir."), ("elevation_username_tip", "Kullanıcı adı veya etki alanı\\kullanıcı adı girin"), ("Preparing for installation ...", "Kuruluma hazırlanıyor..."), ("Show my cursor", "İmlecimi göster"), - ("Scale custom", "Özel boyutlandır"), + ("Scale custom", "Özel ölçekte"), ("Custom scale slider", "Özel ölçek kaydırıcısı"), ("Decrease", "Azalt"), ("Increase", "Arttır"), From 4fa5e99e653c90dd79c5aa2a9ab6df388cc78eea Mon Sep 17 00:00:00 2001 From: Daniel Marschall <28412477+danielmarschall@users.noreply.github.com> Date: Tue, 3 Feb 2026 13:55:34 +0100 Subject: [PATCH 120/277] Remove unused option_env!(...) (#13959) --- src/common.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/common.rs b/src/common.rs index 5f8772414..bba453c34 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1072,10 +1072,6 @@ fn get_api_server_(api: String, custom: String) -> String { if !api.is_empty() { return api.to_owned(); } - let api = option_env!("API_SERVER").unwrap_or_default(); - if !api.is_empty() { - return api.into(); - } let s0 = get_custom_rendezvous_server(custom); if !s0.is_empty() { let s = crate::increase_port(&s0, -2); @@ -1737,8 +1733,7 @@ pub fn create_symmetric_key_msg(their_pk_b: [u8; 32]) -> (Bytes, Bytes, secretbo #[inline] pub fn using_public_server() -> bool { - option_env!("RENDEZVOUS_SERVER").unwrap_or("").is_empty() - && crate::get_custom_rendezvous_server(get_option("custom-rendezvous-server")).is_empty() + crate::get_custom_rendezvous_server(get_option("custom-rendezvous-server")).is_empty() } pub struct ThrottledInterval { From 626a091f55d1c6de9d8c50ed343eaf9e48fa9b50 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 6 Feb 2026 14:18:48 +0800 Subject: [PATCH 121/277] fix(translation): OIDC, Continue with (#14271) Signed-off-by: fufesou --- flutter/lib/common/widgets/login.dart | 2 +- src/lang/ar.rs | 2 +- src/lang/be.rs | 2 +- src/lang/bg.rs | 2 +- src/lang/ca.rs | 2 +- src/lang/cn.rs | 2 +- src/lang/cs.rs | 2 +- src/lang/da.rs | 2 +- src/lang/de.rs | 2 +- src/lang/el.rs | 2 +- src/lang/eo.rs | 2 +- src/lang/es.rs | 2 +- src/lang/et.rs | 2 +- src/lang/eu.rs | 2 +- src/lang/fa.rs | 2 +- src/lang/fi.rs | 2 +- src/lang/fr.rs | 2 +- src/lang/ge.rs | 2 +- src/lang/he.rs | 2 +- src/lang/hr.rs | 2 +- src/lang/hu.rs | 2 +- src/lang/id.rs | 2 +- src/lang/it.rs | 2 +- src/lang/ja.rs | 2 +- src/lang/ko.rs | 2 +- src/lang/kz.rs | 2 +- src/lang/lt.rs | 2 +- src/lang/lv.rs | 2 +- src/lang/nb.rs | 2 +- src/lang/nl.rs | 2 +- src/lang/pl.rs | 2 +- src/lang/pt_PT.rs | 2 +- src/lang/ptbr.rs | 2 +- src/lang/ro.rs | 2 +- src/lang/ru.rs | 2 +- src/lang/sc.rs | 2 +- src/lang/sk.rs | 2 +- src/lang/sl.rs | 2 +- src/lang/sq.rs | 2 +- src/lang/sr.rs | 2 +- src/lang/sv.rs | 2 +- src/lang/ta.rs | 2 +- src/lang/template.rs | 2 +- src/lang/th.rs | 2 +- src/lang/tr.rs | 2 +- src/lang/tw.rs | 2 +- src/lang/uk.rs | 2 +- src/lang/vi.rs | 2 +- 48 files changed, 48 insertions(+), 48 deletions(-) diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart index 5fafc87b9..62ade8e51 100644 --- a/flutter/lib/common/widgets/login.dart +++ b/flutter/lib/common/widgets/login.dart @@ -103,7 +103,7 @@ class ButtonOP extends StatelessWidget { child: FittedBox( fit: BoxFit.scaleDown, child: Center( - child: Text('${translate("Continue with")} $opLabel')), + child: Text(translate("Continue with {$opLabel}"))), ), ), ], diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 14f74f048..65853847a 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "عرض RustDesk"), ("This PC", "هذا الحاسب"), ("or", "او"), - ("Continue with", "متابعة مع"), ("Elevate", "ارتقاء"), ("Zoom cursor", "تكبير المؤشر"), ("Accept sessions via password", "قبول الجلسات عبر كلمة المرور"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "متابعة مع {}"), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 7e0322deb..0b8492e9c 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "Паказаць RustDesk"), ("This PC", "Гэты кампутар"), ("or", "або"), - ("Continue with", "Працягнуць з"), ("Elevate", "Павысіць"), ("Zoom cursor", "Павялічэнне курсора"), ("Accept sessions via password", "Прымаць сеансы па паролю"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Працягнуць з {}"), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 573a7824e..986b7b1fb 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "Покажи RustDesk"), ("This PC", "Този компютър"), ("or", "или"), - ("Continue with", "Продължи с"), ("Elevate", "Повишаване"), ("Zoom cursor", "Уголемяване курсор"), ("Accept sessions via password", "Приемане сесии чрез парола"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Продължи с {}"), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index adbbd3d09..3a7d5498e 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "Mostra el RustDesk"), ("This PC", "Aquest equip"), ("or", "o"), - ("Continue with", "Continua amb"), ("Elevate", "Permisos ampliats"), ("Zoom cursor", "Escala del ratolí"), ("Accept sessions via password", "Accepta les sessions mitjançant una contrasenya"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Continua amb {}"), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 24a2bf5cc..516015390 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "显示 RustDesk"), ("This PC", "此电脑"), ("or", "或"), - ("Continue with", "使用"), ("Elevate", "提权"), ("Zoom cursor", "缩放光标"), ("Accept sessions via password", "只允许密码访问"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", "更新日志"), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "使用 {} 登录"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index ff8b9856a..497af5cf1 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "Zobrazit RustDesk"), ("This PC", "Tento počítač"), ("or", "nebo"), - ("Continue with", "Pokračovat s"), ("Elevate", "Zvýšit"), ("Zoom cursor", "Kurzor přiblížení"), ("Accept sessions via password", "Přijímat relace pomocí hesla"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Pokračovat s {}"), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 9d0b6960a..6505f2bdf 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "Vis RustDesk"), ("This PC", "Denne PC"), ("or", "eller"), - ("Continue with", "Fortsæt med"), ("Elevate", "Elevér"), ("Zoom cursor", "Zoom markør"), ("Accept sessions via password", "Acceptér sessioner via adgangskode"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Fortsæt med {}"), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index b0757e223..5ada5b270 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "RustDesk anzeigen"), ("This PC", "Dieser PC"), ("or", "oder"), - ("Continue with", "Fortfahren mit"), ("Elevate", "Zugriff gewähren"), ("Zoom cursor", "Cursor vergrößern"), ("Accept sessions via password", "Sitzung mit Passwort bestätigen"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", "Änderungsprotokoll"), ("keep-awake-during-outgoing-sessions-label", "Bildschirm während ausgehender Sitzungen aktiv halten"), ("keep-awake-during-incoming-sessions-label", "Bildschirm während eingehender Sitzungen aktiv halten"), + ("Continue with {}", "Fortfahren mit {}"), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index caf0b4566..1542a8ee1 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "Εμφάνιση RustDesk"), ("This PC", "Αυτός ο υπολογιστής"), ("or", "ή"), - ("Continue with", "Συνέχεια με"), ("Elevate", "Ανύψωση"), ("Zoom cursor", "Kέρσορας μεγέθυνσης"), ("Accept sessions via password", "Αποδοχή συνεδριών με κωδικό πρόσβασης"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Συνέχεια με {}"), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 5edd85ccf..303fc45a8 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", ""), ("This PC", ""), ("or", ""), - ("Continue with", ""), ("Elevate", ""), ("Zoom cursor", ""), ("Accept sessions via password", ""), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index a6e010568..bceff6a56 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "Mostrar RustDesk"), ("This PC", "Este PC"), ("or", "o"), - ("Continue with", "Continuar con"), ("Elevate", "Elevar privilegios"), ("Zoom cursor", "Ampliar cursor"), ("Accept sessions via password", "Aceptar sesiones a través de contraseña"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Continuar con {}"), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index 910db4df7..4d87490ac 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "Kuva RustDesk"), ("This PC", "See arvuti"), ("or", "või"), - ("Continue with", "Jätka koos"), ("Elevate", "Tõsta"), ("Zoom cursor", "Suumi kursorit"), ("Accept sessions via password", "Aktsepteeri seansid parooli kaudu"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Jätka koos {}"), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index daaedb24c..ba0979fe7 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "Erakutsi RustDesk"), ("This PC", "PC hau"), ("or", "edo"), - ("Continue with", "Jarraitu honekin"), ("Elevate", "Igo maila"), ("Zoom cursor", "Handitu kurtsorea"), ("Accept sessions via password", "Onartu saioak pasahitzaren bidez"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "{} honekin jarraitu"), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 47df53bc9..5fe019444 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "RustDesk نمایش"), ("This PC", "This PC"), ("or", "یا"), - ("Continue with", "ادامه با"), ("Elevate", "ارتقاء"), ("Zoom cursor", " بزرگنمایی نشانگر ماوس"), ("Accept sessions via password", "قبول درخواست با رمز عبور"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "ادامه با {}"), ].iter().cloned().collect(); } diff --git a/src/lang/fi.rs b/src/lang/fi.rs index d63d8ce20..59f25538b 100644 --- a/src/lang/fi.rs +++ b/src/lang/fi.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "Näytä RustDesk"), ("This PC", "Tämä tietokone"), ("or", "tai"), - ("Continue with", "Jatka käyttäen"), ("Elevate", "Korota oikeudet"), ("Zoom cursor", "Suurennusosoitin"), ("Accept sessions via password", "Hyväksy istunnot salasanalla"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Jatka käyttäen {}"), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 9b56726d5..9637233aa 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "Afficher RustDesk"), ("This PC", "Ce PC"), ("or", "ou"), - ("Continue with", "Continuer avec"), ("Elevate", "Élever les privilèges"), ("Zoom cursor", "Augmenter la taille du curseur"), ("Accept sessions via password", "Accepter les sessions via mot de passe"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", "Journal des modifications"), ("keep-awake-during-outgoing-sessions-label", "Maintenir l’écran allumé lors des sessions sortantes"), ("keep-awake-during-incoming-sessions-label", "Maintenir l’écran allumé lors des sessions entrantes"), + ("Continue with {}", "Continuer avec {}"), ].iter().cloned().collect(); } diff --git a/src/lang/ge.rs b/src/lang/ge.rs index 178906587..ffb9e351d 100644 --- a/src/lang/ge.rs +++ b/src/lang/ge.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "RustDesk-ის ჩვენება"), ("This PC", "ეს კომპიუტერი"), ("or", "ან"), - ("Continue with", "გაგრძელება"), ("Elevate", "უფლებების აწევა"), ("Zoom cursor", "კურსორის მასშტაბირება"), ("Accept sessions via password", "სესიების მიღება პაროლით"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "{}-ით გაგრძელება"), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 3a58c1235..74b93c155 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "הצג את RustDesk"), ("This PC", "מחשב זה"), ("or", "או"), - ("Continue with", "המשך עם"), ("Elevate", "הפעל הרשאות מורחבות"), ("Zoom cursor", "הגדל סמן"), ("Accept sessions via password", "קבל הפעלות באמצעות סיסמה"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "המשך עם {}"), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index b946ab2de..8232b8635 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "Prikaži RustDesk"), ("This PC", "Ovo računalo"), ("or", "ili"), - ("Continue with", "Nastavi sa"), ("Elevate", "Izdigni"), ("Zoom cursor", "Zumiraj kursor"), ("Accept sessions via password", "Prihvati sesije preko lozinke"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Nastavi sa {}"), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index c06400cbf..c9f5453b9 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "A RustDesk megjelenítése"), ("This PC", "Ez a számítógép"), ("or", "vagy"), - ("Continue with", "Folytatás a következővel"), ("Elevate", "Hozzáférés engedélyezése"), ("Zoom cursor", "Kurzor nagyítása"), ("Accept sessions via password", "Munkamenetek elfogadása jelszóval"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", "Változáslista"), ("keep-awake-during-outgoing-sessions-label", "Képernyő aktív állapotban tartása a kimenő munkamenetek során"), ("keep-awake-during-incoming-sessions-label", "Képernyő aktív állapotban tartása a bejövő munkamenetek során"), + ("Continue with {}", "Folytatás a következővel: {}"), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index d4e6290ac..f7498dd99 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "Tampilkan RustDesk"), ("This PC", "PC ini"), ("or", "atau"), - ("Continue with", "Lanjutkan dengan"), ("Elevate", "Elevasi"), ("Zoom cursor", "Perbersar Kursor"), ("Accept sessions via password", "Izinkan sesi dengan kata sandi"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Lanjutkan dengan {}"), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index f83232a0f..eabfac559 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "Visualizza RustDesk"), ("This PC", "Questo PC"), ("or", "O"), - ("Continue with", "Continua con"), ("Elevate", "Eleva"), ("Zoom cursor", "Cursore zoom"), ("Accept sessions via password", "Accetta sessioni via password"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", "Novità programma"), ("keep-awake-during-outgoing-sessions-label", "Mantieni lo schermo attivo durante le sessioni in uscita"), ("keep-awake-during-incoming-sessions-label", "Mantieni lo schermo attivo durante le sessioni in ingresso"), + ("Continue with {}", "Continua con {}"), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 989432c87..c89899469 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "RustDesk を表示"), ("This PC", "この PC"), ("or", "または"), - ("Continue with", "で続行"), ("Elevate", "昇格"), ("Zoom cursor", "カーソルを拡大する"), ("Accept sessions via password", "パスワードでセッションを承認"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "{} で続行"), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 21fcb7661..0acb29a3d 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "RustDesk 표시"), ("This PC", "이 PC"), ("or", "또는"), - ("Continue with", "계속"), ("Elevate", "권한 상승"), ("Zoom cursor", "커서 확대/축소"), ("Accept sessions via password", "비밀번호를 통해 세션 수락"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", "변경 기록"), ("keep-awake-during-outgoing-sessions-label", "발신 세션 중 화면 켜짐 유지"), ("keep-awake-during-incoming-sessions-label", "수신 세션 중 화면 켜짐 유지"), + ("Continue with {}", "{} (으)로 계속"), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 74a709f46..eaa0bb34d 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", ""), ("This PC", ""), ("or", ""), - ("Continue with", ""), ("Elevate", ""), ("Zoom cursor", ""), ("Accept sessions via password", ""), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index fd0c0df77..18080ee77 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "Rodyti RustDesk"), ("This PC", "Šis kompiuteris"), ("or", "arba"), - ("Continue with", "Tęsti su"), ("Elevate", "Pakelti"), ("Zoom cursor", "Mastelio keitimo žymeklis"), ("Accept sessions via password", "Priimti seansus naudojant slaptažodį"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Tęsti su {}"), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 820b67f1e..12b90d8f1 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "Rādīt RustDesk"), ("This PC", "Šis dators"), ("or", "vai"), - ("Continue with", "Turpināt ar"), ("Elevate", "Pacelt"), ("Zoom cursor", "Tālummaiņas kursors"), ("Accept sessions via password", "Pieņemt sesijas, izmantojot paroli"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Turpināt ar {}"), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index e812b174b..b118a4b7c 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "Vis RustDesk"), ("This PC", "Denne PC"), ("or", "eller"), - ("Continue with", "Fortsett med"), ("Elevate", "Elever"), ("Zoom cursor", "Zoom markør"), ("Accept sessions via password", "Aksepter sesjoner via passord"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Fortsett med {}"), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 34e35615f..f952a844e 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "Toon RustDesk"), ("This PC", "Deze PC"), ("or", "of"), - ("Continue with", "Ga verder met"), ("Elevate", "Verhoog"), ("Zoom cursor", "Zoom cursor"), ("Accept sessions via password", "Sessies accepteren via wachtwoord"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", "Wijzigingenlogboek"), ("keep-awake-during-outgoing-sessions-label", "Houd het scherm open tijdens de uitgaande sessies."), ("keep-awake-during-incoming-sessions-label", "Houd het scherm open tijdens de inkomende sessies."), + ("Continue with {}", "Ga verder met {}"), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 6ce5b98fa..6d2185e47 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "Pokaż RustDesk"), ("This PC", "Ten komputer"), ("or", "lub"), - ("Continue with", "Kontynuuj z"), ("Elevate", "Uzyskaj uprawnienia"), ("Zoom cursor", "Powiększenie kursora"), ("Accept sessions via password", "Uwierzytelnij sesję używając hasła"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", "Dziennik zmian"), ("keep-awake-during-outgoing-sessions-label", "Utrzymuj urządzenie w stanie aktywnym podczas sesji wychodzących"), ("keep-awake-during-incoming-sessions-label", "Utrzymuj urządzenie w stanie aktywnym podczas sesji przychodzących"), + ("Continue with {}", "Kontynuuj z {}"), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 0a851273c..6a3e49817 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", ""), ("This PC", ""), ("or", ""), - ("Continue with", ""), ("Elevate", ""), ("Zoom cursor", ""), ("Accept sessions via password", ""), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index e26d6b2c8..c709faeba 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "Exibir RustDesk"), ("This PC", "Este Computador"), ("or", "ou"), - ("Continue with", "Continuar com"), ("Elevate", "Elevar"), ("Zoom cursor", "Aumentar cursor"), ("Accept sessions via password", "Aceitar sessões via senha"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", "Registro de alterações"), ("keep-awake-during-outgoing-sessions-label", "Manter tela ativa durante sessões de saída"), ("keep-awake-during-incoming-sessions-label", "Manter tela ativa durante sessões de entrada"), + ("Continue with {}", "Continuar com {}"), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 54469bfda..9c21617d7 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "Afișează RustDesk"), ("This PC", "Acest PC"), ("or", "sau"), - ("Continue with", "Continuă cu"), ("Elevate", "Sporește privilegii"), ("Zoom cursor", "Cursor lupă"), ("Accept sessions via password", "Acceptă începerea sesiunii folosind parola"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Continuă cu {}"), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 74c3f1358..f4ae05e99 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "Показать RustDesk"), ("This PC", "Этот компьютер"), ("or", "или"), - ("Continue with", "Продолжить с"), ("Elevate", "Повысить"), ("Zoom cursor", "Масштабировать курсор"), ("Accept sessions via password", "Принимать сеансы по паролю"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", "Журнал изменений"), ("keep-awake-during-outgoing-sessions-label", "Не отключать экран во время исходящих сеансов"), ("keep-awake-during-incoming-sessions-label", "Не отключать экран во время входящих сеансов"), + ("Continue with {}", "Продолжить с {}"), ].iter().cloned().collect(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index ef1e160b2..46c4c582e 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "Mustra RustDesk"), ("This PC", "Custu PC"), ("or", "O"), - ("Continue with", "Sighi cun"), ("Elevate", "Cresche"), ("Zoom cursor", "Cursore de ismanniamentu"), ("Accept sessions via password", "Atzeta sessiones cun sa crae"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Sighi cun {}"), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 75ef252e9..85cd17594 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "Zobraziť RustDesk"), ("This PC", "Tento počítač"), ("or", "alebo"), - ("Continue with", "Pokračovať s"), ("Elevate", "Zvýšiť"), ("Zoom cursor", "Kurzor priblíženia"), ("Accept sessions via password", "Prijímanie relácií pomocou hesla"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Pokračovať s {}"), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index eb757f613..9c7dead43 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "Prikaži RustDesk"), ("This PC", "Ta računalnik"), ("or", "ali"), - ("Continue with", "Nadaljuj z"), ("Elevate", "Povzdig pravic"), ("Zoom cursor", "Prilagodi velikost miškinega kazalca"), ("Accept sessions via password", "Sprejmi seje z geslom"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Nadaljuj z {}"), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index adf64a108..b4f4fb694 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "Shfaq RustDesk"), ("This PC", "Ky PC"), ("or", "ose"), - ("Continue with", "Vazhdo me"), ("Elevate", "Ngritja"), ("Zoom cursor", "Zmadho kursorin"), ("Accept sessions via password", "Prano sesionin nëpërmjet fjalëkalimit"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Vazhdo me {}"), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index ae2170c28..a12fc3311 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "Prikazi RustDesk"), ("This PC", "Ovaj PC"), ("or", "ili"), - ("Continue with", "Nastavi sa"), ("Elevate", "Izdigni"), ("Zoom cursor", "Zumiraj kursor"), ("Accept sessions via password", "Prihvati sesije preko lozinke"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Nastavi sa {}"), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 917306a30..f85e88853 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "Visa RustDesk"), ("This PC", "Denna dator"), ("or", "eller"), - ("Continue with", "Fortsätt med"), ("Elevate", "Höj upp"), ("Zoom cursor", "Zoom"), ("Accept sessions via password", "Acceptera sessioner via lösenord"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Fortsätt med {}"), ].iter().cloned().collect(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs index 460b0dca9..4f545f055 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "RustDesk ஐ காட்டு"), ("This PC", "இந்த PC"), ("or", "அல்லது"), - ("Continue with", "உடன் தொடர்"), ("Elevate", "உயர்த்து"), ("Zoom cursor", "கர்சரை பெரிதாக்கு"), ("Accept sessions via password", "கடவுச்சொல் வழியாக அமர்வுகளை ஏற்று"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "{} உடன் தொடர்"), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 936eef3e1..c9aec1a3e 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", ""), ("This PC", ""), ("or", ""), - ("Continue with", ""), ("Elevate", ""), ("Zoom cursor", ""), ("Accept sessions via password", ""), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index a36b7f61b..6d66b44fd 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "แสดง RustDesk"), ("This PC", "พีซีเครื่องนี้"), ("or", "หรือ"), - ("Continue with", "ทำต่อด้วย"), ("Elevate", "ยกระดับ"), ("Zoom cursor", "ขยายเคอร์เซอร์"), ("Accept sessions via password", "ยอมรับการเชื่อมต่อด้วยรหัสผ่าน"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "ทำต่อด้วย {}"), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 08f8de37f..319b631cd 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "RustDesk'i Göster"), ("This PC", "Bu PC"), ("or", "veya"), - ("Continue with", "Bununla devam et"), ("Elevate", "Yükseltme"), ("Zoom cursor", "Yakınlaştırma imleci"), ("Accept sessions via password", "Oturumları parola ile kabul etme"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", "Değişiklik Günlüğü"), ("keep-awake-during-outgoing-sessions-label", "Giden oturumlar süresince ekranı açık tutun"), ("keep-awake-during-incoming-sessions-label", "Gelen oturumlar süresince ekranı açık tutun"), + ("Continue with {}", "{} ile devam et"), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index c4067feec..b66567e43 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "顯示 RustDesk"), ("This PC", "此電腦"), ("or", "或"), - ("Continue with", "繼續"), ("Elevate", "提升權限"), ("Zoom cursor", "縮放游標"), ("Accept sessions via password", "只允許透過輸入密碼進行連線"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", "更新日誌"), ("keep-awake-during-outgoing-sessions-label", "在連出工作階段期間保持螢幕喚醒"), ("keep-awake-during-incoming-sessions-label", "在連入工作階段期間保持螢幕喚醒"), + ("Continue with {}", "使用 {} 登入"), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 8c2acdd3e..bf95a02f7 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "Показати RustDesk"), ("This PC", "Цей ПК"), ("or", "чи"), - ("Continue with", "Продовжити з"), ("Elevate", "Розширення прав"), ("Zoom cursor", "Збільшити вказівник"), ("Accept sessions via password", "Підтверджувати сеанси паролем"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", ""), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Продовжити з {}"), ].iter().cloned().collect(); } diff --git a/src/lang/vi.rs b/src/lang/vi.rs index 4f9611840..1e64c6234 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -384,7 +384,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show RustDesk", "Hiện RustDesk"), ("This PC", "Máy tính này"), ("or", "hoặc"), - ("Continue with", "Tiếp tục với"), ("Elevate", "Nâng quyền"), ("Zoom cursor", "Phóng to con trỏ"), ("Accept sessions via password", "Chấp nhận phiên qua mật khẩu"), @@ -739,5 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", "Nhật ký thay đổi"), ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), + ("Continue with {}", "Tiếp tục với {}"), ].iter().cloned().collect(); } From 0118e1613279bae1ef5f4c2818d1b1f2b7221cc7 Mon Sep 17 00:00:00 2001 From: Hugo Breda <11139838+agarre@users.noreply.github.com> Date: Sun, 8 Feb 2026 13:31:47 -0300 Subject: [PATCH 122/277] PT-BR language update (#14295) * PT-BR language update @rustdesk Please merge. Thanks * Update ptbr.rs * Update ptbr.rs Please submit, i will get back soon and finish all other stuff. * PT-BR language update Completed all missing PT-BR translations. --- src/lang/ptbr.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index c709faeba..e16f7ba61 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -673,21 +673,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("dont-show-again-tip", "Não mostrar novamente"), ("Take screenshot", "Capturar de tela"), ("Taking screenshot", "Capturando tela"), - ("screenshot-merged-screen-not-supported-tip", ""), - ("screenshot-action-tip", ""), + ("screenshot-merged-screen-not-supported-tip", "Mesclar a captura de tela de múltiplos monitores não é suportada no momento. Por favor, alterne para um único monitor e tente novamente."), + ("screenshot-action-tip", "Por favor, selecione como seguir com a captura de tela."), ("Save as", "Salvar como"), ("Copy to clipboard", "Copiar para área de transferência"), ("Enable remote printer", "Habilitar impressora remota"), - ("Downloading {}", ""), - ("{} Update", ""), - ("{}-to-update-tip", ""), + ("Downloading {}", "Baixando {}"), + ("{} Update", "Atualização do {}"), + ("{}-to-update-tip", "{} será fechado agora para instalar a nova versão."), ("download-new-version-failed-tip", "Falha no download. Você pode tentar novamente ou clicar no botão \"Download\" para baixar da página releases e atualizar manualmente."), ("Auto update", "Atualização automática"), ("update-failed-check-msi-tip", "Falha na verificação do método de instalação. Clique no botão \"Download\" para baixar da página releases e atualizar manualmente."), ("websocket_tip", "Usando WebSocket, apenas conexões via relay são suportadas."), ("Use WebSocket", "Usar WebSocket"), ("Trackpad speed", "Velocidade do trackpad"), - ("Default trackpad speed", ""), + ("Default trackpad speed", "Velocidade padrão do trackpad"), ("Numeric one-time password", "Senha numérica de uso único"), ("Enable IPv6 P2P connection", "Habilitar conexão IPv6 P2P"), ("Enable UDP hole punching", "Habilitar UDP hole punching"), @@ -717,11 +717,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Virtual mouse size", "Tamanho do mouse virtual"), ("Small", "Pequeno"), ("Large", "Grande"), - ("Show virtual joystick", ""), + ("Show virtual joystick", "Mostrar joystick virtual"), ("Edit note", "Editar nota"), ("Alias", "Apelido"), ("ScrollEdge", "Rolagem nas bordas"), - ("Allow insecure TLS fallback", ""), + ("Allow insecure TLS fallback", "Permitir fallback TLS inseguro"), ("allow-insecure-tls-fallback-tip", "Por padrão, o RustDesk verifica o certificado do servidor para protocolos que usam TLS.\nCom esta opção habilitada, o RustDesk ignorará a verificação e prosseguirá em caso de falha."), ("Disable UDP", "Desabilitar UDP"), ("disable-udp-tip", "Controla se deve usar somente TCP.\nCom esta opção habilitada, o RustDesk não usará mais UDP 21116, TCP 21116 será usado no lugar."), From 54eae37038a9d3f64afbc9bc243dea207e85cae6 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 9 Feb 2026 00:36:25 +0800 Subject: [PATCH 123/277] fix(ios): workaround physical keyboard after virtual keyboard hidden (#14207) Signed-off-by: fufesou --- flutter/lib/mobile/pages/remote_page.dart | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 1850f2093..9c8ffed65 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -68,6 +68,7 @@ class _RemotePageState extends State with WidgetsBindingObserver { double _viewInsetsBottom = 0; final _uniqueKey = UniqueKey(); Timer? _timerDidChangeMetrics; + Timer? _iosKeyboardWorkaroundTimer; final _blockableOverlayState = BlockableOverlayState(); @@ -140,6 +141,7 @@ class _RemotePageState extends State with WidgetsBindingObserver { await gFFI.close(); _timer?.cancel(); _timerDidChangeMetrics?.cancel(); + _iosKeyboardWorkaroundTimer?.cancel(); gFFI.dialogManager.dismissAll(); await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); @@ -206,6 +208,21 @@ class _RemotePageState extends State with WidgetsBindingObserver { gFFI.ffiModel.pi.version.isNotEmpty) { gFFI.invokeMethod("enable_soft_keyboard", false); } + + // Workaround for iOS: physical keyboard input fails after virtual keyboard is hidden + // https://github.com/flutter/flutter/issues/39900 + // https://github.com/rustdesk/rustdesk/discussions/11843#discussioncomment-13499698 - Virtual keyboard issue + if (isIOS) { + _iosKeyboardWorkaroundTimer?.cancel(); + _iosKeyboardWorkaroundTimer = Timer(Duration(milliseconds: 100), () { + if (!mounted) return; + _physicalFocusNode.unfocus(); + _iosKeyboardWorkaroundTimer = Timer(Duration(milliseconds: 50), () { + if (!mounted) return; + _physicalFocusNode.requestFocus(); + }); + }); + } } else { _timer?.cancel(); _timer = Timer(kMobileDelaySoftKeyboardFocus, () { From de6bf9dc7eeece2c4a5b0ee46b42d55cd1e8c499 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 9 Feb 2026 15:54:22 +0800 Subject: [PATCH 124/277] fix(ios): Add defensive timer cancellation for keyboard visibility (#14301) Signed-off-by: fufesou --- flutter/lib/mobile/pages/remote_page.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 9c8ffed65..b379a5591 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -224,6 +224,8 @@ class _RemotePageState extends State with WidgetsBindingObserver { }); } } else { + _iosKeyboardWorkaroundTimer?.cancel(); + _iosKeyboardWorkaroundTimer = null; _timer?.cancel(); _timer = Timer(kMobileDelaySoftKeyboardFocus, () { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, From 067fab2b73f48c25e6f1843d27e8e5457f0a9550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?VenusGirl=E2=9D=A4?= Date: Tue, 10 Feb 2026 19:48:30 +0900 Subject: [PATCH 125/277] Update Korean (#14298) Correct spacing and spelling --- src/lang/ko.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 0acb29a3d..d860af5ab 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -738,6 +738,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", "변경 기록"), ("keep-awake-during-outgoing-sessions-label", "발신 세션 중 화면 켜짐 유지"), ("keep-awake-during-incoming-sessions-label", "수신 세션 중 화면 켜짐 유지"), - ("Continue with {}", "{} (으)로 계속"), + ("Continue with {}", "{}(으)로 계속"), ].iter().cloned().collect(); } From 6c541f7bfd5792da3773fab1082e1e44007bc75b Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 11 Feb 2026 16:11:15 +0800 Subject: [PATCH 126/277] fix(xdo): deb, libxdo3 | libxdo4 (#14314) Signed-off-by: fufesou --- build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.py b/build.py index 87c0dbd34..ce9a09ef6 100755 --- a/build.py +++ b/build.py @@ -299,7 +299,7 @@ Version: %s Architecture: %s Maintainer: rustdesk Homepage: https://rustdesk.com -Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva2, libva-drm2, libva-x11-2, libgstreamer-plugins-base1.0-0, libpam0g, gstreamer1.0-pipewire%s +Depends: libgtk-3-0, libxcb-randr0, libxdo3 | libxdo4, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva2, libva-drm2, libva-x11-2, libgstreamer-plugins-base1.0-0, libpam0g, gstreamer1.0-pipewire%s Recommends: libayatana-appindicator3-1 Description: A remote control software. From 2842315b1d189ec0b9e5ee34954e6095947afb14 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 11 Feb 2026 16:11:47 +0800 Subject: [PATCH 127/277] Fix/linux shortcuts inhibit (#14302) * feat: Inhibit system shortcuts on Linux Fixes #13013. Signed-off-by: Max von Forell * fix(linux): shortcuts inhibit Signed-off-by: fufesou --------- Signed-off-by: Max von Forell Signed-off-by: fufesou Co-authored-by: Max von Forell --- flatpak/rustdesk.json | 1 + .../desktop/pages/desktop_setting_page.dart | 94 +++++++ flutter/linux/CMakeLists.txt | 60 ++++- flutter/linux/my_application.cc | 12 + flutter/linux/wayland_shortcuts_inhibit.cc | 244 ++++++++++++++++++ flutter/linux/wayland_shortcuts_inhibit.h | 22 ++ src/flutter_ffi.rs | 28 ++ src/lang/en.rs | 2 +- src/platform/linux.rs | 119 +++++++++ 9 files changed, 580 insertions(+), 2 deletions(-) create mode 100644 flutter/linux/wayland_shortcuts_inhibit.cc create mode 100644 flutter/linux/wayland_shortcuts_inhibit.h diff --git a/flatpak/rustdesk.json b/flatpak/rustdesk.json index c4935e137..2418ac2a6 100644 --- a/flatpak/rustdesk.json +++ b/flatpak/rustdesk.json @@ -55,6 +55,7 @@ ], "finish-args": [ "--share=ipc", + "--socket=wayland", "--socket=x11", "--share=network", "--filesystem=home", diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index b513bd4d9..b26d909cb 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -2538,6 +2538,49 @@ class WaylandCard extends StatefulWidget { class _WaylandCardState extends State { final restoreTokenKey = 'wayland-restore-token'; + static const _kClearShortcutsInhibitorEventKey = + 'clear-gnome-shortcuts-inhibitor-permission-res'; + final _clearShortcutsInhibitorFailedMsg = ''.obs; + // Don't show the shortcuts permission reset button for now. + // Users can change it manually: + // "Settings" -> "Apps" -> "RustDesk" -> "Permissions" -> "Inhibit Shortcuts". + // For resetting(clearing) the permission from the portal permission store, you can + // use (replace with the RustDesk desktop file ID): + // busctl --user call org.freedesktop.impl.portal.PermissionStore \ + // /org/freedesktop/impl/portal/PermissionStore org.freedesktop.impl.portal.PermissionStore \ + // DeletePermission sss "gnome" "shortcuts-inhibitor" "" + // On a native install this is typically "rustdesk.desktop"; on Flatpak it is usually + // the exported desktop ID derived from the Flatpak app-id (e.g. "com.rustdesk.RustDesk.desktop"). + // + // We may add it back in the future if needed. + final showResetInhibitorPermission = false; + + @override + void initState() { + super.initState(); + if (showResetInhibitorPermission) { + platformFFI.registerEventHandler( + _kClearShortcutsInhibitorEventKey, _kClearShortcutsInhibitorEventKey, + (evt) async { + if (!mounted) return; + if (evt['success'] == true) { + setState(() {}); + } else { + _clearShortcutsInhibitorFailedMsg.value = + evt['msg'] as String? ?? 'Unknown error'; + } + }); + } + } + + @override + void dispose() { + if (showResetInhibitorPermission) { + platformFFI.unregisterEventHandler( + _kClearShortcutsInhibitorEventKey, _kClearShortcutsInhibitorEventKey); + } + super.dispose(); + } @override Widget build(BuildContext context) { @@ -2545,9 +2588,16 @@ class _WaylandCardState extends State { future: bind.mainHandleWaylandScreencastRestoreToken( key: restoreTokenKey, value: "get"), hasData: (restoreToken) { + final hasShortcutsPermission = showResetInhibitorPermission && + bind.mainGetCommonSync( + key: "has-gnome-shortcuts-inhibitor-permission") == + "true"; + final children = [ if (restoreToken.isNotEmpty) _buildClearScreenSelection(context, restoreToken), + if (hasShortcutsPermission) + _buildClearShortcutsInhibitorPermission(context), ]; return Offstage( offstage: children.isEmpty, @@ -2592,6 +2642,50 @@ class _WaylandCardState extends State { ), ); } + + Widget _buildClearShortcutsInhibitorPermission(BuildContext context) { + onConfirm() { + _clearShortcutsInhibitorFailedMsg.value = ''; + bind.mainSetCommon( + key: "clear-gnome-shortcuts-inhibitor-permission", value: ""); + gFFI.dialogManager.dismissAll(); + } + + showConfirmMsgBox() => msgBoxCommon( + gFFI.dialogManager, + 'Confirmation', + Text( + translate('confirm-clear-shortcuts-inhibitor-permission-tip'), + ), + [ + dialogButton('OK', onPressed: onConfirm), + dialogButton('Cancel', + onPressed: () => gFFI.dialogManager.dismissAll()) + ]); + + return Column(children: [ + Obx( + () => _clearShortcutsInhibitorFailedMsg.value.isEmpty + ? Offstage() + : Align( + alignment: Alignment.topLeft, + child: Text(_clearShortcutsInhibitorFailedMsg.value, + style: DefaultTextStyle.of(context) + .style + .copyWith(color: Colors.red)) + .marginOnly(bottom: 10.0)), + ), + _Button( + 'Reset keyboard shortcuts permission', + showConfirmMsgBox, + tip: 'clear-shortcuts-inhibitor-permission-tip', + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all( + Theme.of(context).colorScheme.error.withOpacity(0.75)), + ), + ), + ]); + } } // ignore: non_constant_identifier_names diff --git a/flutter/linux/CMakeLists.txt b/flutter/linux/CMakeLists.txt index d320f403c..56a8dbb70 100644 --- a/flutter/linux/CMakeLists.txt +++ b/flutter/linux/CMakeLists.txt @@ -1,6 +1,6 @@ # Project-level configuration. cmake_minimum_required(VERSION 3.10) -project(runner LANGUAGES CXX) +project(runner LANGUAGES C CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. @@ -54,6 +54,55 @@ add_subdirectory(${FLUTTER_MANAGED_DIR}) find_package(PkgConfig REQUIRED) pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +# Wayland protocol for keyboard shortcuts inhibit +pkg_check_modules(WAYLAND_CLIENT IMPORTED_TARGET wayland-client) +pkg_check_modules(WAYLAND_PROTOCOLS_PKG QUIET wayland-protocols) +pkg_check_modules(WAYLAND_SCANNER_PKG QUIET wayland-scanner) + +if(WAYLAND_PROTOCOLS_PKG_FOUND) + pkg_get_variable(WAYLAND_PROTOCOLS_DIR wayland-protocols pkgdatadir) +endif() +if(WAYLAND_SCANNER_PKG_FOUND) + pkg_get_variable(WAYLAND_SCANNER wayland-scanner wayland_scanner) +endif() + +if(WAYLAND_CLIENT_FOUND AND WAYLAND_PROTOCOLS_DIR AND WAYLAND_SCANNER) + set(KEYBOARD_SHORTCUTS_INHIBIT_PROTOCOL + "${WAYLAND_PROTOCOLS_DIR}/unstable/keyboard-shortcuts-inhibit/keyboard-shortcuts-inhibit-unstable-v1.xml") + + if(EXISTS ${KEYBOARD_SHORTCUTS_INHIBIT_PROTOCOL}) + set(WAYLAND_GENERATED_DIR "${CMAKE_CURRENT_BINARY_DIR}/wayland-protocols") + file(MAKE_DIRECTORY ${WAYLAND_GENERATED_DIR}) + + # Generate client header + add_custom_command( + OUTPUT "${WAYLAND_GENERATED_DIR}/keyboard-shortcuts-inhibit-unstable-v1-client-protocol.h" + COMMAND ${WAYLAND_SCANNER} client-header + ${KEYBOARD_SHORTCUTS_INHIBIT_PROTOCOL} + "${WAYLAND_GENERATED_DIR}/keyboard-shortcuts-inhibit-unstable-v1-client-protocol.h" + DEPENDS ${KEYBOARD_SHORTCUTS_INHIBIT_PROTOCOL} + VERBATIM + ) + + # Generate protocol code + add_custom_command( + OUTPUT "${WAYLAND_GENERATED_DIR}/keyboard-shortcuts-inhibit-unstable-v1-protocol.c" + COMMAND ${WAYLAND_SCANNER} private-code + ${KEYBOARD_SHORTCUTS_INHIBIT_PROTOCOL} + "${WAYLAND_GENERATED_DIR}/keyboard-shortcuts-inhibit-unstable-v1-protocol.c" + DEPENDS ${KEYBOARD_SHORTCUTS_INHIBIT_PROTOCOL} + VERBATIM + ) + + set(WAYLAND_PROTOCOL_SOURCES + "${WAYLAND_GENERATED_DIR}/keyboard-shortcuts-inhibit-unstable-v1-client-protocol.h" + "${WAYLAND_GENERATED_DIR}/keyboard-shortcuts-inhibit-unstable-v1-protocol.c" + ) + + set(HAS_KEYBOARD_SHORTCUTS_INHIBIT TRUE) + endif() +endif() + add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") # Define the application target. To change its name, change BINARY_NAME above, @@ -63,9 +112,11 @@ add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") add_executable(${BINARY_NAME} "main.cc" "my_application.cc" + "wayland_shortcuts_inhibit.cc" "bump_mouse.cc" "bump_mouse_x11.cc" "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + ${WAYLAND_PROTOCOL_SOURCES} ) # Apply the standard set of build settings. This can be removed for applications @@ -78,6 +129,13 @@ target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) target_link_libraries(${BINARY_NAME} PRIVATE ${CMAKE_DL_LIBS}) # target_link_libraries(${BINARY_NAME} PRIVATE librustdesk) +# Wayland support for keyboard shortcuts inhibit +if(HAS_KEYBOARD_SHORTCUTS_INHIBIT) + target_compile_definitions(${BINARY_NAME} PRIVATE HAS_KEYBOARD_SHORTCUTS_INHIBIT) + target_include_directories(${BINARY_NAME} PRIVATE ${WAYLAND_GENERATED_DIR}) + target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::WAYLAND_CLIENT) +endif() + # Run the Flutter tool portions of the build. This must not be removed. add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc index c84cbddba..a05bb7856 100644 --- a/flutter/linux/my_application.cc +++ b/flutter/linux/my_application.cc @@ -6,6 +6,11 @@ #ifdef GDK_WINDOWING_X11 #include #endif +#if defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT) +#include "wayland_shortcuts_inhibit.h" +#endif + +#include #include "flutter/generated_plugin_registrant.h" @@ -91,6 +96,13 @@ static void my_application_activate(GApplication* application) { gtk_widget_show(GTK_WIDGET(window)); gtk_widget_show(GTK_WIDGET(view)); +#if defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT) + // Register callback for sub-windows created by desktop_multi_window plugin + // Only sub-windows (remote windows) need keyboard shortcuts inhibition + desktop_multi_window_plugin_set_window_created_callback( + (WindowCreatedCallback)wayland_shortcuts_inhibit_init_for_subwindow); +#endif + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new(); diff --git a/flutter/linux/wayland_shortcuts_inhibit.cc b/flutter/linux/wayland_shortcuts_inhibit.cc new file mode 100644 index 000000000..76c45be4d --- /dev/null +++ b/flutter/linux/wayland_shortcuts_inhibit.cc @@ -0,0 +1,244 @@ +// Wayland keyboard shortcuts inhibit implementation +// Uses the zwp_keyboard_shortcuts_inhibit_manager_v1 protocol to request +// the compositor to disable system shortcuts for specific windows. + +#include "wayland_shortcuts_inhibit.h" + +#if defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT) + +#include +#include +#include +#include "keyboard-shortcuts-inhibit-unstable-v1-client-protocol.h" + +// Data structure to hold inhibitor state for each window +typedef struct { + struct zwp_keyboard_shortcuts_inhibit_manager_v1* manager; + struct zwp_keyboard_shortcuts_inhibitor_v1* inhibitor; +} ShortcutsInhibitData; + +// Cleanup function for ShortcutsInhibitData +static void shortcuts_inhibit_data_free(gpointer data) { + ShortcutsInhibitData* inhibit_data = static_cast(data); + if (inhibit_data->inhibitor != NULL) { + zwp_keyboard_shortcuts_inhibitor_v1_destroy(inhibit_data->inhibitor); + } + if (inhibit_data->manager != NULL) { + zwp_keyboard_shortcuts_inhibit_manager_v1_destroy(inhibit_data->manager); + } + g_free(inhibit_data); +} + +// Wayland registry handler to find the shortcuts inhibit manager +static void registry_handle_global(void* data, struct wl_registry* registry, + uint32_t name, const char* interface, + uint32_t /*version*/) { + ShortcutsInhibitData* inhibit_data = static_cast(data); + if (strcmp(interface, + zwp_keyboard_shortcuts_inhibit_manager_v1_interface.name) == 0) { + inhibit_data->manager = + static_cast(wl_registry_bind( + registry, name, &zwp_keyboard_shortcuts_inhibit_manager_v1_interface, + 1)); + } +} + +static void registry_handle_global_remove(void* /*data*/, struct wl_registry* /*registry*/, + uint32_t /*name*/) { + // Not needed for this use case +} + +static const struct wl_registry_listener registry_listener = { + registry_handle_global, + registry_handle_global_remove, +}; + +// Inhibitor event handlers +static void inhibitor_active(void* /*data*/, + struct zwp_keyboard_shortcuts_inhibitor_v1* /*inhibitor*/) { + // Inhibitor is now active, shortcuts are being captured +} + +static void inhibitor_inactive(void* /*data*/, + struct zwp_keyboard_shortcuts_inhibitor_v1* /*inhibitor*/) { + // Inhibitor is now inactive, shortcuts restored to compositor +} + +static const struct zwp_keyboard_shortcuts_inhibitor_v1_listener inhibitor_listener = { + inhibitor_active, + inhibitor_inactive, +}; + +// Forward declaration +static void uninhibit_keyboard_shortcuts(GtkWindow* window); + +// Inhibit keyboard shortcuts on Wayland for a specific window +static void inhibit_keyboard_shortcuts(GtkWindow* window) { + GdkDisplay* display = gtk_widget_get_display(GTK_WIDGET(window)); + if (!GDK_IS_WAYLAND_DISPLAY(display)) { + return; + } + + // Check if already inhibited for this window + if (g_object_get_data(G_OBJECT(window), "shortcuts-inhibit-data") != NULL) { + return; + } + + ShortcutsInhibitData* inhibit_data = g_new0(ShortcutsInhibitData, 1); + + struct wl_display* wl_display = gdk_wayland_display_get_wl_display(display); + if (wl_display == NULL) { + shortcuts_inhibit_data_free(inhibit_data); + return; + } + + struct wl_registry* registry = wl_display_get_registry(wl_display); + if (registry == NULL) { + shortcuts_inhibit_data_free(inhibit_data); + return; + } + + wl_registry_add_listener(registry, ®istry_listener, inhibit_data); + wl_display_roundtrip(wl_display); + + if (inhibit_data->manager == NULL) { + wl_registry_destroy(registry); + shortcuts_inhibit_data_free(inhibit_data); + return; + } + + GdkWindow* gdk_window = gtk_widget_get_window(GTK_WIDGET(window)); + if (gdk_window == NULL) { + wl_registry_destroy(registry); + shortcuts_inhibit_data_free(inhibit_data); + return; + } + + struct wl_surface* surface = gdk_wayland_window_get_wl_surface(gdk_window); + if (surface == NULL) { + wl_registry_destroy(registry); + shortcuts_inhibit_data_free(inhibit_data); + return; + } + + GdkSeat* gdk_seat = gdk_display_get_default_seat(display); + if (gdk_seat == NULL) { + wl_registry_destroy(registry); + shortcuts_inhibit_data_free(inhibit_data); + return; + } + + struct wl_seat* seat = gdk_wayland_seat_get_wl_seat(gdk_seat); + if (seat == NULL) { + wl_registry_destroy(registry); + shortcuts_inhibit_data_free(inhibit_data); + return; + } + + inhibit_data->inhibitor = + zwp_keyboard_shortcuts_inhibit_manager_v1_inhibit_shortcuts( + inhibit_data->manager, surface, seat); + + if (inhibit_data->inhibitor == NULL) { + wl_registry_destroy(registry); + shortcuts_inhibit_data_free(inhibit_data); + return; + } + + // Add listener to monitor active/inactive state + zwp_keyboard_shortcuts_inhibitor_v1_add_listener( + inhibit_data->inhibitor, &inhibitor_listener, window); + + wl_display_roundtrip(wl_display); + wl_registry_destroy(registry); + + // Associate the inhibit data with the window for cleanup on destroy + g_object_set_data_full(G_OBJECT(window), "shortcuts-inhibit-data", + inhibit_data, shortcuts_inhibit_data_free); +} + +// Remove keyboard shortcuts inhibitor from a window +static void uninhibit_keyboard_shortcuts(GtkWindow* window) { + ShortcutsInhibitData* inhibit_data = static_cast( + g_object_get_data(G_OBJECT(window), "shortcuts-inhibit-data")); + + if (inhibit_data == NULL) { + return; + } + + // This will trigger shortcuts_inhibit_data_free via g_object_set_data + g_object_set_data(G_OBJECT(window), "shortcuts-inhibit-data", NULL); +} + +// Focus event handlers for dynamic inhibitor management +static gboolean on_window_focus_in(GtkWidget* widget, GdkEventFocus* /*event*/, gpointer /*user_data*/) { + if (GTK_IS_WINDOW(widget)) { + inhibit_keyboard_shortcuts(GTK_WINDOW(widget)); + } + return FALSE; // Continue event propagation +} + +static gboolean on_window_focus_out(GtkWidget* widget, GdkEventFocus* /*event*/, gpointer /*user_data*/) { + if (GTK_IS_WINDOW(widget)) { + uninhibit_keyboard_shortcuts(GTK_WINDOW(widget)); + } + return FALSE; // Continue event propagation +} + +// Key for marking window as having focus handlers connected +static const char* const kFocusHandlersConnectedKey = "shortcuts-inhibit-focus-handlers-connected"; +// Key for marking window as having a pending realize handler +static const char* const kRealizeHandlerConnectedKey = "shortcuts-inhibit-realize-handler-connected"; + +// Callback when window is realized (mapped to screen) +// Sets up focus-based inhibitor management +static void on_window_realize(GtkWidget* widget, gpointer /*user_data*/) { + if (GTK_IS_WINDOW(widget)) { + // Check if focus handlers are already connected to avoid duplicates + if (g_object_get_data(G_OBJECT(widget), kFocusHandlersConnectedKey) != NULL) { + return; + } + + // Connect focus events for dynamic inhibitor management + g_signal_connect(widget, "focus-in-event", + G_CALLBACK(on_window_focus_in), NULL); + g_signal_connect(widget, "focus-out-event", + G_CALLBACK(on_window_focus_out), NULL); + + // Mark as connected to prevent duplicate connections + g_object_set_data(G_OBJECT(widget), kFocusHandlersConnectedKey, GINT_TO_POINTER(1)); + + // If window already has focus, create inhibitor now + if (gtk_window_has_toplevel_focus(GTK_WINDOW(widget))) { + inhibit_keyboard_shortcuts(GTK_WINDOW(widget)); + } + } +} + +// Public API: Initialize shortcuts inhibit for a sub-window +void wayland_shortcuts_inhibit_init_for_subwindow(void* view) { + GtkWidget* widget = GTK_WIDGET(view); + GtkWidget* toplevel = gtk_widget_get_toplevel(widget); + + if (toplevel != NULL && GTK_IS_WINDOW(toplevel)) { + // Check if already initialized to avoid duplicate realize handlers + if (g_object_get_data(G_OBJECT(toplevel), kFocusHandlersConnectedKey) != NULL || + g_object_get_data(G_OBJECT(toplevel), kRealizeHandlerConnectedKey) != NULL) { + return; + } + + if (gtk_widget_get_realized(toplevel)) { + // Window is already realized, set up focus handlers now + on_window_realize(toplevel, NULL); + } else { + // Mark realize handler as connected to prevent duplicate connections + // if called again before window is realized + g_object_set_data(G_OBJECT(toplevel), kRealizeHandlerConnectedKey, GINT_TO_POINTER(1)); + // Wait for window to be realized + g_signal_connect(toplevel, "realize", + G_CALLBACK(on_window_realize), NULL); + } + } +} + +#endif // defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT) diff --git a/flutter/linux/wayland_shortcuts_inhibit.h b/flutter/linux/wayland_shortcuts_inhibit.h new file mode 100644 index 000000000..c0996931a --- /dev/null +++ b/flutter/linux/wayland_shortcuts_inhibit.h @@ -0,0 +1,22 @@ +// Wayland keyboard shortcuts inhibit support +// This module provides functionality to inhibit system keyboard shortcuts +// on Wayland compositors, allowing remote desktop windows to capture all +// key events including Super, Alt+Tab, etc. + +#ifndef WAYLAND_SHORTCUTS_INHIBIT_H_ +#define WAYLAND_SHORTCUTS_INHIBIT_H_ + +#include + +#if defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT) + +// Initialize shortcuts inhibit for a sub-window created by desktop_multi_window plugin. +// This sets up focus-based inhibitor management: inhibitor is created when +// the window gains focus and destroyed when it loses focus. +// +// @param view The FlView of the sub-window +void wayland_shortcuts_inhibit_init_for_subwindow(void* view); + +#endif // defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT) + +#endif // WAYLAND_SHORTCUTS_INHIBIT_H_ diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index a46cfd8b6..864002d24 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -2759,6 +2759,11 @@ pub fn main_get_common(key: String) -> String { None => "", } .to_string(); + } else if key == "has-gnome-shortcuts-inhibitor-permission" { + #[cfg(target_os = "linux")] + return crate::platform::linux::has_gnome_shortcuts_inhibitor_permission().to_string(); + #[cfg(not(target_os = "linux"))] + return false.to_string(); } else { if key.starts_with("download-data-") { let id = key.replace("download-data-", ""); @@ -2920,6 +2925,29 @@ pub fn main_set_common(_key: String, _value: String) { } else if _key == "cancel-downloader" { crate::hbbs_http::downloader::cancel(&_value); } + + #[cfg(target_os = "linux")] + if _key == "clear-gnome-shortcuts-inhibitor-permission" { + std::thread::spawn(move || { + let (success, msg) = + match crate::platform::linux::clear_gnome_shortcuts_inhibitor_permission() { + Ok(_) => (true, "".to_owned()), + Err(e) => (false, e.to_string()), + }; + let data = HashMap::from([ + ( + "name", + serde_json::json!("clear-gnome-shortcuts-inhibitor-permission-res"), + ), + ("success", serde_json::json!(success)), + ("msg", serde_json::json!(msg)), + ]); + let _res = flutter::push_global_event( + flutter::APP_TYPE_MAIN, + serde_json::ser::to_string(&data).unwrap_or("".to_owned()), + ); + }); + } } pub fn session_get_common_sync( diff --git a/src/lang/en.rs b/src/lang/en.rs index 1399601de..511ddff4a 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -220,7 +220,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("default_proxy_tip", "Default protocol and port are Socks5 and 1080"), ("no_audio_input_device_tip", "No audio input device found."), ("clear_Wayland_screen_selection_tip", "After clearing the screen selection, you can reselect the screen to share."), - ("confirm_clear_Wayland_screen_selection_tip", "Are you sure to clear the Wayland screen selection?"), + ("confirm_clear_Wayland_screen_selection_tip", "Are you sure you want to clear the Wayland screen selection?"), ("android_new_voice_call_tip", "A new voice call request was received. If you accept, the audio will switch to voice communication."), ("texture_render_tip", "Use texture rendering to make the pictures smoother. You could try disabling this option if you encounter rendering issues."), ("floating_window_tip", "It helps to keep RustDesk background service"), diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 382af72cf..9493e1cae 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -2088,3 +2088,122 @@ pub fn is_selinux_enforcing() -> bool { }, } } + +/// Get the app ID for shortcuts inhibitor permission. +/// Returns different ID based on whether running in Flatpak or native. +/// The ID must match the installed .desktop filename, as GNOME Shell's +/// inhibitShortcutsDialog uses `Shell.WindowTracker.get_window_app(window).get_id()`. +fn get_shortcuts_inhibitor_app_id() -> String { + if is_flatpak() { + // In Flatpak, FLATPAK_ID is set automatically by the runtime to the app ID + // (e.g., "com.rustdesk.RustDesk"). This is the most reliable source. + // Fall back to constructing from app name if not available. + match std::env::var("FLATPAK_ID") { + Ok(id) if !id.is_empty() => format!("{}.desktop", id), + _ => { + let app_name = crate::get_app_name(); + format!("com.{}.{}.desktop", app_name.to_lowercase(), app_name) + } + } + } else { + format!("{}.desktop", crate::get_app_name().to_lowercase()) + } +} + +const PERMISSION_STORE_DEST: &str = "org.freedesktop.impl.portal.PermissionStore"; +const PERMISSION_STORE_PATH: &str = "/org/freedesktop/impl/portal/PermissionStore"; +const PERMISSION_STORE_IFACE: &str = "org.freedesktop.impl.portal.PermissionStore"; + +/// Clear GNOME shortcuts inhibitor permission via D-Bus. +/// This allows the permission dialog to be shown again. +pub fn clear_gnome_shortcuts_inhibitor_permission() -> ResultType<()> { + let app_id = get_shortcuts_inhibitor_app_id(); + log::info!( + "Clearing shortcuts inhibitor permission for app_id: {}, is_flatpak: {}", + app_id, + is_flatpak() + ); + + let conn = dbus::blocking::Connection::new_session()?; + let proxy = conn.with_proxy( + PERMISSION_STORE_DEST, + PERMISSION_STORE_PATH, + std::time::Duration::from_secs(3), + ); + + // DeletePermission(s table, s id, s app) -> () + let result: Result<(), dbus::Error> = proxy.method_call( + PERMISSION_STORE_IFACE, + "DeletePermission", + ("gnome", "shortcuts-inhibitor", app_id.as_str()), + ); + + match result { + Ok(()) => { + log::info!("Successfully cleared GNOME shortcuts inhibitor permission"); + Ok(()) + } + Err(e) => { + let err_name = e.name().unwrap_or(""); + // If the permission doesn't exist, that's also fine + if err_name == "org.freedesktop.portal.Error.NotFound" + || err_name == "org.freedesktop.DBus.Error.UnknownObject" + || err_name == "org.freedesktop.DBus.Error.ServiceUnknown" + { + log::info!("GNOME shortcuts inhibitor permission was not set ({})", err_name); + Ok(()) + } else { + bail!("Failed to clear permission: {}", e) + } + } + } +} + +/// Check if GNOME shortcuts inhibitor permission exists. +pub fn has_gnome_shortcuts_inhibitor_permission() -> bool { + let app_id = get_shortcuts_inhibitor_app_id(); + + let conn = match dbus::blocking::Connection::new_session() { + Ok(c) => c, + Err(e) => { + log::debug!("Failed to connect to session bus: {}", e); + return false; + } + }; + let proxy = conn.with_proxy( + PERMISSION_STORE_DEST, + PERMISSION_STORE_PATH, + std::time::Duration::from_secs(3), + ); + + // Lookup(s table, s id) -> (a{sas} permissions, v data) + // We only need the permissions dict; check if app_id is a key. + let result: Result< + ( + std::collections::HashMap>, + dbus::arg::Variant>, + ), + dbus::Error, + > = proxy.method_call( + PERMISSION_STORE_IFACE, + "Lookup", + ("gnome", "shortcuts-inhibitor"), + ); + + match result { + Ok((permissions, _)) => { + let found = permissions.contains_key(&app_id); + log::debug!( + "Shortcuts inhibitor permission lookup: app_id={}, found={}, keys={:?}", + app_id, + found, + permissions.keys().collect::>() + ); + found + } + Err(e) => { + log::debug!("Failed to query shortcuts inhibitor permission: {}", e); + false + } + } +} From 85db6779828349b23ca3eba91cc7cd36c5337797 Mon Sep 17 00:00:00 2001 From: Shaikh Naasir Date: Fri, 13 Feb 2026 22:36:25 +0530 Subject: [PATCH 128/277] docs: fix typos in clipboard documentation (#13521) Signed-off-by: Naasir --- libs/clipboard/README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/libs/clipboard/README.md b/libs/clipboard/README.md index 6333a0644..ec08cbf04 100644 --- a/libs/clipboard/README.md +++ b/libs/clipboard/README.md @@ -10,7 +10,7 @@ TODO: Move this lib to a separate project. ## How it works -Terminalogies: +Terminologies: - cliprdr: this module - local: the endpoint which initiates a file copy events @@ -50,7 +50,7 @@ sequenceDiagram r ->> l: Format List Response (notified) r ->> l: Format Data Request (requests file list) activate l - note left of l: Retrive file list from system clipboard + note left of l: Retrieve file list from system clipboard l ->> r: Format Data Response (containing file list) deactivate l note over r: Update system clipboard with received file list @@ -84,10 +84,10 @@ and copy files to remote. The protocol was originally designed as an extension of the Windows RDP, so the specific message packages fits windows well. -When starting cliprdr, a thread is spawn to create a invisible window +When starting cliprdr, a thread is spawned to create an invisible window and to subscribe to OLE clipboard events. The window's callback (see `cliprdr_proc` in `src/windows/wf_cliprdr.c`) was -set to handle a variaty of events. +set to handle a variety of events. Detailed implementation is shown in pictures above. @@ -108,18 +108,18 @@ after filtering out those pointing to our FUSE directory or duplicated, send format list directly to remote. The cliprdr server also uses clipboard client for setting clipboard, -or retrive paths from system. +or retrieve paths from system. #### Local File List -The local file list is a temperary list of file metadata. +The local file list is a temporary list of file metadata. When receiving file contents PDU from peer, the server picks out the file requested and open it for reading if necessary. Also when receiving Format Data Request PDU from remote asking for file list, the local file list should be rebuilt from file list retrieved from Clipboard Client. -Some caching and preloading could done on it since applications are likely to read +Some caching and preloading could be done on it since applications are likely to read on the list sequentially. #### FUSE server From 980bc11e68487cccb0f3e31c3acccd878cb0ef61 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 14 Feb 2026 17:48:53 +0800 Subject: [PATCH 129/277] update common --- libs/hbb_common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/hbb_common b/libs/hbb_common index 900077a2c..da339dca6 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 900077a2c2651336317f8094ea44074c48acd2a4 +Subproject commit da339dca64ecae3273838c0a1395c7fe2f1a1016 From 40f86fa6390a243929c200e03aa1dc8b2d510a21 Mon Sep 17 00:00:00 2001 From: Vance <40771709+vancez@users.noreply.github.com> Date: Sun, 15 Feb 2026 14:52:27 +0800 Subject: [PATCH 130/277] fix(mobile): account for safe area padding in canvas size calculation (#14285) * fix(mobile): account for safe area padding in canvas size calculation * fix(mobile): differentiate safe area handling for portrait vs landscape * refact(ios): Simple refactor Signed-off-by: fufesou * fix(ios): canvas getSize, test -> Android Signed-off-by: fufesou * fix: comments Signed-off-by: fufesou --------- Signed-off-by: fufesou Co-authored-by: fufesou --- flutter/lib/models/model.dart | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 7a3f98377..ff298c380 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -2215,10 +2215,32 @@ class CanvasModel with ChangeNotifier { double w = size.width - leftToEdge - rightToEdge; double h = size.height - topToEdge - bottomToEdge; if (isMobile) { + // Account for horizontal safe area insets on both orientations. + w = w - mediaData.padding.left - mediaData.padding.right; + // Vertically, subtract the bottom keyboard inset (viewInsets.bottom) and any + // bottom overlay (e.g. key-help tools) so the canvas is not covered. h = h - mediaData.viewInsets.bottom - (parent.target?.cursorModel.keyHelpToolsRectToAdjustCanvas?.bottom ?? 0); + // Orientation-specific handling: + // - Portrait: additionally subtract top padding (e.g. status bar / notch) + // - Landscape: does not subtract mediaData.padding.top/bottom (home indicator auto-hides) + final isPortrait = size.height > size.width; + if (isPortrait) { + // In portrait mode, subtract the top safe-area padding (e.g. status bar / notch) + // so the remote image is not truncated, while keeping the bottom inset to avoid + // introducing unnecessary blank space around the canvas. + // + // iOS -> Android, portrait, adjust mode: + // h = h (no padding subtracted): top and bottom are truncated + // https://github.com/user-attachments/assets/30ed4559-c27e-432b-847f-8fec23c9f998 + // h = h - top - bottom: extra blank spaces appear + // https://github.com/user-attachments/assets/12a98817-3b4e-43aa-be0f-4b03cf364b7e + // h = h - top (current): works fine + // https://github.com/user-attachments/assets/95f047f2-7f47-4a36-8113-5023989a0c81 + h = h - mediaData.padding.top; + } } return Size(w < 0 ? 0 : w, h < 0 ? 0 : h); } From b268aa106188052cf7faf802ff9221c6d028974b Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 15 Feb 2026 16:12:26 +0800 Subject: [PATCH 131/277] Fix some single device multiple ids scenarios on MacOS (#14196) * fix(macos): sync config to root when root config is empty Signed-off-by: 21pages * fix(server): gate startup on initial config sync; document CheckIfResendPk limitation - wait up to 3s for initial root->local config sync before starting server services - continue startup when timeout is hit, while keeping sync/watch running in background - avoid blocking non-server process startup - clarify that CheckIfResendPk only re-registers PK for current ID and does not solve multi-ID when root uses a non-default mac-generated ID Signed-off-by: 21pages --------- Signed-off-by: 21pages --- src/rendezvous_mediator.rs | 27 +++++++++++++++ src/server.rs | 67 +++++++++++++++++++++++++++++++++++--- 2 files changed, 90 insertions(+), 4 deletions(-) diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 5d26d3389..b3ab6a523 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -40,6 +40,7 @@ lazy_static::lazy_static! { } static SHOULD_EXIT: AtomicBool = AtomicBool::new(false); static MANUAL_RESTARTED: AtomicBool = AtomicBool::new(false); +static SENT_REGISTER_PK: AtomicBool = AtomicBool::new(false); #[derive(Clone)] pub struct RendezvousMediator { @@ -689,6 +690,7 @@ impl RendezvousMediator { ..Default::default() }); socket.send(&msg_out).await?; + SENT_REGISTER_PK.store(true, Ordering::SeqCst); Ok(()) } @@ -904,3 +906,28 @@ async fn udp_nat_listen( })?; Ok(()) } + +// When config is not yet synced from root, register_pk may have already been sent with a new generated pk. +// After config sync completes, the pk may change. This struct detects pk changes and triggers +// a re-registration by setting key_confirmed to false. +// NOTE: +// This only corrects PK registration for the current ID. If root uses a non-default mac-generated ID, +// this does not resolve the multi-ID issue by itself. +pub struct CheckIfResendPk { + pk: Option>, +} +impl CheckIfResendPk { + pub fn new() -> Self { + Self { + pk: Config::get_cached_pk(), + } + } +} +impl Drop for CheckIfResendPk { + fn drop(&mut self) { + if SENT_REGISTER_PK.load(Ordering::SeqCst) && Config::get_cached_pk() != self.pk { + Config::set_key_confirmed(false); + log::info!("Set key_confirmed to false due to pk changed, will resend register_pk"); + } + } +} diff --git a/src/server.rs b/src/server.rs index 5dc504fe9..dddc762bf 100644 --- a/src/server.rs +++ b/src/server.rs @@ -82,6 +82,10 @@ type ConnMap = HashMap; #[cfg(any(target_os = "macos", target_os = "linux"))] const CONFIG_SYNC_INTERVAL_SECS: f32 = 0.3; +#[cfg(any(target_os = "macos", target_os = "linux"))] +// 3s is enough for at least one initial sync attempt: +// 0.3s backoff + up to 1s connect timeout + up to 1s response timeout. +const CONFIG_SYNC_INITIAL_WAIT_SECS: u64 = 3; lazy_static::lazy_static! { pub static ref CHILD_PROCESS: Childs = Default::default(); @@ -600,7 +604,7 @@ pub async fn start_server(is_server: bool, no_server: bool) { allow_err!(input_service::setup_uinput(0, 1920, 0, 1080).await); } #[cfg(any(target_os = "macos", target_os = "linux"))] - tokio::spawn(async { sync_and_watch_config_dir().await }); + wait_initial_config_sync().await; #[cfg(target_os = "windows")] crate::platform::try_kill_broker(); #[cfg(feature = "hwcodec")] @@ -685,13 +689,43 @@ pub async fn start_ipc_url_server() { } #[cfg(any(target_os = "macos", target_os = "linux"))] -async fn sync_and_watch_config_dir() { +async fn wait_initial_config_sync() { if crate::platform::is_root() { return; } + // Non-server process should not block startup, but still keeps background sync/watch alive. + if !crate::is_server() { + tokio::spawn(async move { + sync_and_watch_config_dir(None).await; + }); + return; + } + + let (sync_done_tx, mut sync_done_rx) = tokio::sync::oneshot::channel::<()>(); + tokio::spawn(async move { + sync_and_watch_config_dir(Some(sync_done_tx)).await; + }); + + // Server process waits up to N seconds for initial root->local sync to reduce stale-start window. + tokio::select! { + _ = &mut sync_done_rx => { + } + _ = tokio::time::sleep(Duration::from_secs(CONFIG_SYNC_INITIAL_WAIT_SECS)) => { + log::warn!( + "timed out waiting {}s for initial config sync, continue startup and keep syncing in background", + CONFIG_SYNC_INITIAL_WAIT_SECS + ); + } + } +} + +#[cfg(any(target_os = "macos", target_os = "linux"))] +async fn sync_and_watch_config_dir(sync_done_tx: Option>) { let mut cfg0 = (Config::get(), Config2::get()); let mut synced = false; + let mut is_root_config_empty = false; + let mut sync_done_tx = sync_done_tx; let tries = if crate::is_server() { 30 } else { 3 }; log::debug!("#tries of ipc service connection: {}", tries); use hbb_common::sleep; @@ -706,6 +740,8 @@ async fn sync_and_watch_config_dir() { Data::SyncConfig(Some(configs)) => { let (config, config2) = *configs; let _chk = crate::ipc::CheckIfRestart::new(); + #[cfg(target_os = "macos")] + let _chk_pk = crate::CheckIfResendPk::new(); if !config.is_empty() { if cfg0.0 != config { cfg0.0 = config.clone(); @@ -717,8 +753,20 @@ async fn sync_and_watch_config_dir() { Config2::set(config2); log::info!("sync config2 from root"); } + } else { + // only on macos, because this issue was only reproduced on macos + #[cfg(target_os = "macos")] + { + // root config is empty, mark for sync in watch loop + // to prevent root from generating a new config on login screen + is_root_config_empty = true; + } } synced = true; + // Notify startup waiter once initial sync phase finishes successfully. + if let Some(tx) = sync_done_tx.take() { + let _ = tx.send(()); + } } _ => {} }; @@ -729,8 +777,14 @@ async fn sync_and_watch_config_dir() { loop { sleep(CONFIG_SYNC_INTERVAL_SECS).await; let cfg = (Config::get(), Config2::get()); - if cfg != cfg0 { - log::info!("config updated, sync to root"); + let should_sync = + cfg != cfg0 || (is_root_config_empty && !cfg.0.is_empty()); + if should_sync { + if is_root_config_empty { + log::info!("root config is empty, sync our config to root"); + } else { + log::info!("config updated, sync to root"); + } match conn.send(&Data::SyncConfig(Some(cfg.clone().into()))).await { Err(e) => { log::error!("sync config to root failed: {}", e); @@ -745,6 +799,7 @@ async fn sync_and_watch_config_dir() { _ => { cfg0 = cfg; conn.next_timeout(1000).await.ok(); + is_root_config_empty = false; } } } @@ -755,6 +810,10 @@ async fn sync_and_watch_config_dir() { } } } + // Notify startup waiter even when initial sync is skipped/failed, to avoid unnecessary waiting. + if let Some(tx) = sync_done_tx.take() { + let _ = tx.send(()); + } log::warn!("skipped config sync"); } From 779b7aaf0265b0fe22e1d71b364c405db3d2231e Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sun, 15 Feb 2026 16:43:21 +0800 Subject: [PATCH 132/277] feat(wayland): keyboard mode, legacy translate (#14317) Signed-off-by: fufesou --- .../lib/desktop/widgets/remote_toolbar.dart | 14 +- libs/enigo/src/linux/nix_impl.rs | 20 +- src/server/input_service.rs | 319 ++++++++++++++- src/server/rdp_input.rs | 369 ++++++++++++++++-- src/server/uinput.rs | 250 ++++++++++-- 5 files changed, 897 insertions(+), 75 deletions(-) diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 8146e0d6f..ec05c987f 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -1861,8 +1861,18 @@ class _KeyboardMenu extends StatelessWidget { continue; } - if (pi.isWayland && mode.key != kKeyMapMode) { - continue; + if (pi.isWayland) { + // Legacy mode is hidden on desktop control side because dead keys + // don't work properly on Wayland. When the control side is mobile, + // Legacy mode is used automatically (mobile always sends Legacy events). + if (mode.key == kKeyLegacyMode) { + continue; + } + // Translate mode requires server >= 1.4.6. + if (mode.key == kKeyTranslateMode && + versionCmp(pi.version, '1.4.6') < 0) { + continue; + } } var text = translate(mode.menu); diff --git a/libs/enigo/src/linux/nix_impl.rs b/libs/enigo/src/linux/nix_impl.rs index 902d77948..c16be3469 100644 --- a/libs/enigo/src/linux/nix_impl.rs +++ b/libs/enigo/src/linux/nix_impl.rs @@ -261,6 +261,8 @@ impl KeyboardControllable for Enigo { } else { if let Some(keyboard) = &mut self.custom_keyboard { keyboard.key_sequence(sequence) + } else { + log::warn!("Enigo::key_sequence: no custom_keyboard set for Wayland!"); } } } @@ -277,6 +279,7 @@ impl KeyboardControllable for Enigo { if let Some(keyboard) = &mut self.custom_keyboard { keyboard.key_down(key) } else { + log::warn!("Enigo::key_down: no custom_keyboard set for Wayland!"); Ok(()) } } @@ -290,13 +293,24 @@ impl KeyboardControllable for Enigo { } else { if let Some(keyboard) = &mut self.custom_keyboard { keyboard.key_up(key) + } else { + log::warn!("Enigo::key_up: no custom_keyboard set for Wayland!"); } } } fn key_click(&mut self, key: Key) { - if self.tfc_key_click(key).is_err() { - self.key_down(key).ok(); - self.key_up(key); + if self.is_x11 { + // X11: try tfc first, then fallback to key_down/key_up + if self.tfc_key_click(key).is_err() { + self.key_down(key).ok(); + self.key_up(key); + } + } else { + if let Some(keyboard) = &mut self.custom_keyboard { + keyboard.key_click(key); + } else { + log::warn!("Enigo::key_click: no custom_keyboard set for Wayland!"); + } } } } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index b1c2d66b6..fb8441dde 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -111,6 +111,10 @@ struct Input { const KEY_CHAR_START: u64 = 9999; +// XKB keycode for Insert key (evdev KEY_INSERT code 110 + 8 for XKB offset) +#[cfg(target_os = "linux")] +const XKB_KEY_INSERT: u16 = evdev::Key::KEY_INSERT.code() + 8; + #[derive(Clone, Default)] pub struct MouseCursorSub { inner: ConnInner, @@ -1105,8 +1109,12 @@ pub fn handle_mouse_simulation_(evt: &MouseEvent, conn: i32) { // Clamp delta to prevent extreme/malicious values from reaching OS APIs. // This matches the Flutter client's kMaxRelativeMouseDelta constant. const MAX_RELATIVE_MOUSE_DELTA: i32 = 10000; - let dx = evt.x.clamp(-MAX_RELATIVE_MOUSE_DELTA, MAX_RELATIVE_MOUSE_DELTA); - let dy = evt.y.clamp(-MAX_RELATIVE_MOUSE_DELTA, MAX_RELATIVE_MOUSE_DELTA); + let dx = evt + .x + .clamp(-MAX_RELATIVE_MOUSE_DELTA, MAX_RELATIVE_MOUSE_DELTA); + let dy = evt + .y + .clamp(-MAX_RELATIVE_MOUSE_DELTA, MAX_RELATIVE_MOUSE_DELTA); en.mouse_move_relative(dx, dy); // Get actual cursor position after relative movement for tracking if let Some((x, y)) = crate::get_cursor_pos() { @@ -1465,20 +1473,26 @@ fn map_keyboard_mode(evt: &KeyEvent) { // Wayland #[cfg(target_os = "linux")] if !crate::platform::linux::is_x11() { - let mut en = ENIGO.lock().unwrap(); - let code = evt.chr() as u16; - - if evt.down { - en.key_down(enigo::Key::Raw(code)).ok(); - } else { - en.key_up(enigo::Key::Raw(code)); - } + wayland_send_raw_key(evt.chr() as u16, evt.down); return; } sim_rdev_rawkey_position(evt.chr() as _, evt.down); } +/// Send raw keycode on Wayland via the active backend (uinput or RemoteDesktop portal). +/// The keycode is expected to be a Linux keycode (evdev code + 8 for X11 compatibility). +#[cfg(target_os = "linux")] +#[inline] +fn wayland_send_raw_key(code: u16, down: bool) { + let mut en = ENIGO.lock().unwrap(); + if down { + en.key_down(enigo::Key::Raw(code)).ok(); + } else { + en.key_up(enigo::Key::Raw(code)); + } +} + #[cfg(target_os = "macos")] fn add_flags_to_enigo(en: &mut Enigo, key_event: &KeyEvent) { // When long-pressed the command key, then press and release @@ -1559,6 +1573,20 @@ fn need_to_uppercase(en: &mut Enigo) -> bool { } fn process_chr(en: &mut Enigo, chr: u32, down: bool) { + // On Wayland with uinput mode, use clipboard for character input + #[cfg(target_os = "linux")] + if !crate::platform::linux::is_x11() && wayland_use_uinput() { + // Skip clipboard for hotkeys (Ctrl/Alt/Meta pressed) + if !is_hotkey_modifier_pressed(en) { + if down { + if let Ok(c) = char::try_from(chr) { + input_char_via_clipboard_server(en, c); + } + } + return; + } + } + let key = char_value_to_key(chr); if down { @@ -1578,15 +1606,136 @@ fn process_chr(en: &mut Enigo, chr: u32, down: bool) { } fn process_unicode(en: &mut Enigo, chr: u32) { + // On Wayland with uinput mode, use clipboard for character input + #[cfg(target_os = "linux")] + if !crate::platform::linux::is_x11() && wayland_use_uinput() { + if let Ok(c) = char::try_from(chr) { + input_char_via_clipboard_server(en, c); + } + return; + } + if let Ok(chr) = char::try_from(chr) { en.key_sequence(&chr.to_string()); } } fn process_seq(en: &mut Enigo, sequence: &str) { + // On Wayland with uinput mode, use clipboard for text input + #[cfg(target_os = "linux")] + if !crate::platform::linux::is_x11() && wayland_use_uinput() { + input_text_via_clipboard_server(en, sequence); + return; + } + en.key_sequence(&sequence); } +/// Delay in milliseconds to wait for clipboard to sync on Wayland. +/// This is an empirical value — Wayland provides no callback or event to confirm +/// clipboard content has been received by the compositor. Under heavy system load, +/// this delay may be insufficient, but there is no reliable alternative mechanism. +#[cfg(target_os = "linux")] +const CLIPBOARD_SYNC_DELAY_MS: u64 = 50; + +/// Internal: Set clipboard content without delay. +/// Returns true if clipboard was set successfully. +#[cfg(target_os = "linux")] +fn set_clipboard_content(text: &str) -> bool { + use arboard::{Clipboard, LinuxClipboardKind, SetExtLinux}; + + let mut clipboard = match Clipboard::new() { + Ok(cb) => cb, + Err(e) => { + log::error!("set_clipboard_content: failed to create clipboard: {:?}", e); + return false; + } + }; + + // Set both CLIPBOARD and PRIMARY selections + // Terminal uses PRIMARY for Shift+Insert, GUI apps use CLIPBOARD + if let Err(e) = clipboard + .set() + .clipboard(LinuxClipboardKind::Clipboard) + .text(text.to_owned()) + { + log::error!("set_clipboard_content: failed to set CLIPBOARD: {:?}", e); + return false; + } + if let Err(e) = clipboard + .set() + .clipboard(LinuxClipboardKind::Primary) + .text(text.to_owned()) + { + log::warn!("set_clipboard_content: failed to set PRIMARY: {:?}", e); + // Continue anyway, CLIPBOARD might work + } + + true +} + +/// Set clipboard content for paste operation (sync version for use in blocking contexts). +/// +/// Note: The original clipboard content is intentionally NOT restored after paste. +/// Restoring clipboard could cause race conditions where subsequent keystrokes +/// might accidentally paste the old clipboard content instead of the intended input. +/// This trade-off prioritizes input reliability over preserving clipboard state. +#[cfg(target_os = "linux")] +#[inline] +pub(super) fn set_clipboard_for_paste_sync(text: &str) -> bool { + if !set_clipboard_content(text) { + return false; + } + std::thread::sleep(std::time::Duration::from_millis(CLIPBOARD_SYNC_DELAY_MS)); + true +} + +/// Check if a character is ASCII printable (0x20-0x7E). +#[cfg(target_os = "linux")] +#[inline] +pub(super) fn is_ascii_printable(c: char) -> bool { + c as u32 >= 0x20 && c as u32 <= 0x7E +} + +/// Input a single character via clipboard + Shift+Insert in server process. +#[cfg(target_os = "linux")] +#[inline] +fn input_char_via_clipboard_server(en: &mut Enigo, chr: char) { + input_text_via_clipboard_server(en, &chr.to_string()); +} + +/// Input text via clipboard + Shift+Insert in server process. +/// Shift+Insert is more universal than Ctrl+V, works in both GUI apps and terminals. +/// +/// Note: Clipboard content is NOT restored after paste - see `set_clipboard_for_paste_sync` for rationale. +#[cfg(target_os = "linux")] +fn input_text_via_clipboard_server(en: &mut Enigo, text: &str) { + if text.is_empty() { + return; + } + if !set_clipboard_for_paste_sync(text) { + return; + } + + // Use ENIGO's custom_keyboard directly to avoid creating new IPC connections + // which would cause excessive logging and keyboard device creation/destruction + if en.key_down(Key::Shift).is_err() { + log::error!("input_text_via_clipboard_server: failed to press Shift, skipping paste"); + return; + } + if en.key_down(Key::Raw(XKB_KEY_INSERT)).is_err() { + log::error!("input_text_via_clipboard_server: failed to press Insert, releasing Shift"); + en.key_up(Key::Shift); + return; + } + en.key_up(Key::Raw(XKB_KEY_INSERT)); + en.key_up(Key::Shift); + + // Brief delay to allow the target application to process the paste event. + // Empirical value — no reliable synchronization mechanism exists on Wayland. + std::thread::sleep(std::time::Duration::from_millis(20)); +} + #[cfg(not(target_os = "macos"))] fn release_keys(en: &mut Enigo, to_release: &Vec) { for key in to_release { @@ -1621,6 +1770,64 @@ fn is_function_key(ck: &EnumOrUnknown) -> bool { return res; } +/// Check if any hotkey modifier (Ctrl/Alt/Meta) is currently pressed. +/// Used to detect hotkey combinations like Ctrl+C, Alt+Tab, etc. +/// +/// Note: Shift is intentionally NOT checked here. Shift+character produces a different +/// character (e.g., Shift+a → 'A'), which is normal text input, not a hotkey. +/// Shift is only relevant as a hotkey modifier when combined with Ctrl/Alt/Meta +/// (e.g., Ctrl+Shift+Z), in which case this function already returns true via Ctrl. +#[cfg(target_os = "linux")] +#[inline] +fn is_hotkey_modifier_pressed(en: &mut Enigo) -> bool { + get_modifier_state(Key::Control, en) + || get_modifier_state(Key::RightControl, en) + || get_modifier_state(Key::Alt, en) + || get_modifier_state(Key::RightAlt, en) + || get_modifier_state(Key::Meta, en) + || get_modifier_state(Key::RWin, en) +} + +/// Release Shift keys before character input in Legacy/Translate mode. +/// In these modes, the character has already been converted by the client, +/// so we should input it directly without Shift modifier affecting the result. +/// +/// Note: Does NOT release Shift if hotkey modifiers (Ctrl/Alt/Meta) are pressed, +/// to preserve combinations like Ctrl+Shift+Z. +#[cfg(target_os = "linux")] +fn release_shift_for_char_input(en: &mut Enigo) { + // Don't release Shift if hotkey modifiers (Ctrl/Alt/Meta) are pressed. + // This preserves combinations like Ctrl+Shift+Z. + if is_hotkey_modifier_pressed(en) { + return; + } + + // In translate mode, the client has already converted the keystroke to a character + // (e.g., Shift+a → 'A'). We release Shift here so the server inputs the character + // directly without Shift affecting the result. + // + // Shift is intentionally NOT restored after input — the client will send an explicit + // Shift key_up event when the user physically releases Shift. Restoring it here would + // cause a brief Shift re-press that could interfere with the next input event. + + let is_x11 = crate::platform::linux::is_x11(); + + if get_modifier_state(Key::Shift, en) { + if !is_x11 { + en.key_up(Key::Shift); + } else { + simulate_(&EventType::KeyRelease(RdevKey::ShiftLeft)); + } + } + if get_modifier_state(Key::RightShift, en) { + if !is_x11 { + en.key_up(Key::RightShift); + } else { + simulate_(&EventType::KeyRelease(RdevKey::ShiftRight)); + } + } +} + fn legacy_keyboard_mode(evt: &KeyEvent) { #[cfg(windows)] crate::platform::windows::try_change_desktop(); @@ -1640,11 +1847,24 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { process_control_key(&mut en, &ck, down) } Some(key_event::Union::Chr(chr)) => { + // For character input in Legacy mode, we need to release Shift first. + // The character has already been converted by the client, so we should + // input it directly without Shift modifier affecting the result. + // Only Ctrl/Alt/Meta should be kept for hotkeys like Ctrl+C. + #[cfg(target_os = "linux")] + release_shift_for_char_input(&mut en); + let record_key = chr as u64 + KEY_CHAR_START; record_pressed_key(KeysDown::EnigoKey(record_key), down); process_chr(&mut en, chr, down) } - Some(key_event::Union::Unicode(chr)) => process_unicode(&mut en, chr), + Some(key_event::Union::Unicode(chr)) => { + // Same as Chr: release Shift for Unicode input + #[cfg(target_os = "linux")] + release_shift_for_char_input(&mut en); + + process_unicode(&mut en, chr) + } Some(key_event::Union::Seq(ref seq)) => process_seq(&mut en, seq), _ => {} } @@ -1665,6 +1885,51 @@ fn translate_process_code(code: u32, down: bool) { fn translate_keyboard_mode(evt: &KeyEvent) { match &evt.union { Some(key_event::Union::Seq(seq)) => { + // On Wayland, handle character input directly in this (--server) process using clipboard. + // This function runs in the --server process (logged-in user session), which has + // WAYLAND_DISPLAY and XDG_RUNTIME_DIR — so clipboard operations work here. + // + // Why not let it go through uinput IPC: + // 1. For uinput mode: the uinput service thread runs in the --service (root) process, + // which typically lacks user session environment. Clipboard operations there are + // unreliable. Handling clipboard here avoids that issue. + // 2. For RDP input mode: Portal's notify_keyboard_keysym API interprets keysyms + // based on its internal modifier state, which may not match our released state. + // Using clipboard bypasses this issue entirely. + #[cfg(target_os = "linux")] + if !crate::platform::linux::is_x11() { + let mut en = ENIGO.lock().unwrap(); + + // Check if this is a hotkey (Ctrl/Alt/Meta pressed) + // For hotkeys, we send character-based key events via Enigo instead of + // using the clipboard. This relies on the local keyboard layout for + // mapping characters to physical keys. + // This assumes client and server use the same keyboard layout (common case). + // Note: For non-Latin keyboards (e.g., Arabic), hotkeys may not work + // correctly if the character cannot be mapped to a key via KEY_MAP_LAYOUT. + // This is a known limitation - most common hotkeys (Ctrl+A/C/V/Z) use Latin + // characters which are mappable on most keyboard layouts. + if is_hotkey_modifier_pressed(&mut en) { + // For hotkeys, send character-based key events via Enigo. + // This relies on the local keyboard layout mapping (KEY_MAP_LAYOUT). + for chr in seq.chars() { + if !is_ascii_printable(chr) { + log::warn!( + "Hotkey with non-ASCII character may not work correctly on non-Latin keyboard layouts" + ); + } + en.key_click(Key::Layout(chr)); + } + return; + } + + // Normal text input: release Shift and use clipboard + release_shift_for_char_input(&mut en); + + input_text_via_clipboard_server(&mut en, seq); + return; + } + // Fr -> US // client: Shift + & => 1(send to remote) // remote: Shift + 1 => ! @@ -1682,11 +1947,16 @@ fn translate_keyboard_mode(evt: &KeyEvent) { #[cfg(target_os = "linux")] let simulate_win_hot_key = false; if !simulate_win_hot_key { - if get_modifier_state(Key::Shift, &mut en) { - simulate_(&EventType::KeyRelease(RdevKey::ShiftLeft)); - } - if get_modifier_state(Key::RightShift, &mut en) { - simulate_(&EventType::KeyRelease(RdevKey::ShiftRight)); + #[cfg(target_os = "linux")] + release_shift_for_char_input(&mut en); + #[cfg(target_os = "windows")] + { + if get_modifier_state(Key::Shift, &mut en) { + simulate_(&EventType::KeyRelease(RdevKey::ShiftLeft)); + } + if get_modifier_state(Key::RightShift, &mut en) { + simulate_(&EventType::KeyRelease(RdevKey::ShiftRight)); + } } } for chr in seq.chars() { @@ -1706,7 +1976,16 @@ fn translate_keyboard_mode(evt: &KeyEvent) { Some(key_event::Union::Chr(..)) => { #[cfg(target_os = "windows")] translate_process_code(evt.chr(), evt.down); - #[cfg(not(target_os = "windows"))] + #[cfg(target_os = "linux")] + { + if !crate::platform::linux::is_x11() { + // Wayland: use uinput to send raw keycode + wayland_send_raw_key(evt.chr() as u16, evt.down); + } else { + sim_rdev_rawkey_position(evt.chr() as _, evt.down); + } + } + #[cfg(target_os = "macos")] sim_rdev_rawkey_position(evt.chr() as _, evt.down); } Some(key_event::Union::Unicode(..)) => { @@ -1717,7 +1996,11 @@ fn translate_keyboard_mode(evt: &KeyEvent) { simulate_win2win_hotkey(*code, evt.down); } _ => { - log::debug!("Unreachable. Unexpected key event {:?}", &evt); + log::debug!( + "Unreachable. Unexpected key event (mode={:?}, down={:?})", + &evt.mode, + &evt.down + ); } } } diff --git a/src/server/rdp_input.rs b/src/server/rdp_input.rs index d9e11aca4..5348f2f24 100644 --- a/src/server/rdp_input.rs +++ b/src/server/rdp_input.rs @@ -1,7 +1,8 @@ -use crate::uinput::service::map_key; +use super::input_service::set_clipboard_for_paste_sync; +use crate::uinput::service::{can_input_via_keysym, char_to_keysym, map_key}; use dbus::{blocking::SyncConnection, Path}; use enigo::{Key, KeyboardControllable, MouseButton, MouseControllable}; -use hbb_common::ResultType; +use hbb_common::{log, ResultType}; use scrap::wayland::pipewire::{get_portal, PwStreamInfo}; use scrap::wayland::remote_desktop_portal::OrgFreedesktopPortalRemoteDesktop as remote_desktop_portal; use std::collections::HashMap; @@ -19,14 +20,74 @@ pub mod client { const PRESSED_DOWN_STATE: u32 = 1; const PRESSED_UP_STATE: u32 = 0; + /// Modifier key state tracking for RDP input. + /// Portal API doesn't provide a way to query key state, so we track it ourselves. + #[derive(Default)] + struct ModifierState { + shift_left: bool, + shift_right: bool, + ctrl_left: bool, + ctrl_right: bool, + alt_left: bool, + alt_right: bool, + meta_left: bool, + meta_right: bool, + } + + impl ModifierState { + fn update(&mut self, key: &Key, down: bool) { + match key { + Key::Shift => self.shift_left = down, + Key::RightShift => self.shift_right = down, + Key::Control => self.ctrl_left = down, + Key::RightControl => self.ctrl_right = down, + Key::Alt => self.alt_left = down, + Key::RightAlt => self.alt_right = down, + Key::Meta | Key::Super | Key::Windows | Key::Command => self.meta_left = down, + Key::RWin => self.meta_right = down, + // Handle raw keycodes for modifier keys (Linux evdev codes + 8) + // In translate mode, modifier keys may be sent as Chr events with raw keycodes. + // The +8 offset converts evdev codes to X11/XKB keycodes. + Key::Raw(code) => { + const EVDEV_OFFSET: u16 = 8; + const KEY_LEFTSHIFT: u16 = evdev::Key::KEY_LEFTSHIFT.code() + EVDEV_OFFSET; + const KEY_RIGHTSHIFT: u16 = evdev::Key::KEY_RIGHTSHIFT.code() + EVDEV_OFFSET; + const KEY_LEFTCTRL: u16 = evdev::Key::KEY_LEFTCTRL.code() + EVDEV_OFFSET; + const KEY_RIGHTCTRL: u16 = evdev::Key::KEY_RIGHTCTRL.code() + EVDEV_OFFSET; + const KEY_LEFTALT: u16 = evdev::Key::KEY_LEFTALT.code() + EVDEV_OFFSET; + const KEY_RIGHTALT: u16 = evdev::Key::KEY_RIGHTALT.code() + EVDEV_OFFSET; + const KEY_LEFTMETA: u16 = evdev::Key::KEY_LEFTMETA.code() + EVDEV_OFFSET; + const KEY_RIGHTMETA: u16 = evdev::Key::KEY_RIGHTMETA.code() + EVDEV_OFFSET; + match *code { + KEY_LEFTSHIFT => self.shift_left = down, + KEY_RIGHTSHIFT => self.shift_right = down, + KEY_LEFTCTRL => self.ctrl_left = down, + KEY_RIGHTCTRL => self.ctrl_right = down, + KEY_LEFTALT => self.alt_left = down, + KEY_RIGHTALT => self.alt_right = down, + KEY_LEFTMETA => self.meta_left = down, + KEY_RIGHTMETA => self.meta_right = down, + _ => {} + } + } + _ => {} + } + } + } + pub struct RdpInputKeyboard { conn: Arc, session: Path<'static>, + modifier_state: ModifierState, } impl RdpInputKeyboard { pub fn new(conn: Arc, session: Path<'static>) -> ResultType { - Ok(Self { conn, session }) + Ok(Self { + conn, + session, + modifier_state: ModifierState::default(), + }) } } @@ -39,29 +100,192 @@ pub mod client { self } - fn get_key_state(&mut self, _: Key) -> bool { - // no api for this - false + fn get_key_state(&mut self, key: Key) -> bool { + // Use tracked modifier state for supported keys + match key { + Key::Shift => self.modifier_state.shift_left, + Key::RightShift => self.modifier_state.shift_right, + Key::Control => self.modifier_state.ctrl_left, + Key::RightControl => self.modifier_state.ctrl_right, + Key::Alt => self.modifier_state.alt_left, + Key::RightAlt => self.modifier_state.alt_right, + Key::Meta | Key::Super | Key::Windows | Key::Command => { + self.modifier_state.meta_left + } + Key::RWin => self.modifier_state.meta_right, + _ => false, + } } fn key_sequence(&mut self, s: &str) { for c in s.chars() { - let key = Key::Layout(c); - let _ = handle_key(true, key, self.conn.clone(), &self.session); - let _ = handle_key(false, key, self.conn.clone(), &self.session); + let keysym = char_to_keysym(c); + // ASCII characters: use keysym + if can_input_via_keysym(c, keysym) { + if let Err(e) = send_keysym(keysym, true, self.conn.clone(), &self.session) { + log::error!("Failed to send keysym down: {:?}", e); + } + if let Err(e) = send_keysym(keysym, false, self.conn.clone(), &self.session) { + log::error!("Failed to send keysym up: {:?}", e); + } + } else { + // Non-ASCII: use clipboard + input_text_via_clipboard(&c.to_string(), self.conn.clone(), &self.session); + } } } fn key_down(&mut self, key: Key) -> enigo::ResultType { - handle_key(true, key, self.conn.clone(), &self.session)?; + if let Key::Layout(chr) = key { + let keysym = char_to_keysym(chr); + // ASCII characters: use keysym + if can_input_via_keysym(chr, keysym) { + send_keysym(keysym, true, self.conn.clone(), &self.session)?; + } else { + // Non-ASCII: use clipboard (complete key press in key_down) + input_text_via_clipboard(&chr.to_string(), self.conn.clone(), &self.session); + } + } else { + handle_key(true, key.clone(), self.conn.clone(), &self.session)?; + // Update modifier state only after successful send — + // if handle_key fails, we don't want stale "pressed" state + // affecting subsequent key event decisions. + self.modifier_state.update(&key, true); + } Ok(()) } + fn key_up(&mut self, key: Key) { - let _ = handle_key(false, key, self.conn.clone(), &self.session); + // Intentionally asymmetric with key_down: update state BEFORE sending. + // On release, we always mark as released even if the send fails below, + // to avoid permanently stuck-modifier state in our tracker. The trade-off + // (tracker says "released" while OS may still have it pressed) is acceptable + // because such failures are rare and subsequent events will resynchronize. + self.modifier_state.update(&key, false); + + if let Key::Layout(chr) = key { + // ASCII characters: send keysym up if we also sent it on key_down + let keysym = char_to_keysym(chr); + if can_input_via_keysym(chr, keysym) { + if let Err(e) = send_keysym(keysym, false, self.conn.clone(), &self.session) + { + log::error!("Failed to send keysym up: {:?}", e); + } + } + // Non-ASCII: already handled completely in key_down via clipboard paste, + // no corresponding release needed (clipboard paste is an atomic operation) + } else { + if let Err(e) = handle_key(false, key, self.conn.clone(), &self.session) { + log::error!("Failed to handle key up: {:?}", e); + } + } } + fn key_click(&mut self, key: Key) { - let _ = handle_key(true, key, self.conn.clone(), &self.session); - let _ = handle_key(false, key, self.conn.clone(), &self.session); + if let Key::Layout(chr) = key { + let keysym = char_to_keysym(chr); + // ASCII characters: use keysym + if can_input_via_keysym(chr, keysym) { + if let Err(e) = send_keysym(keysym, true, self.conn.clone(), &self.session) { + log::error!("Failed to send keysym down: {:?}", e); + } + if let Err(e) = send_keysym(keysym, false, self.conn.clone(), &self.session) { + log::error!("Failed to send keysym up: {:?}", e); + } + } else { + // Non-ASCII: use clipboard + input_text_via_clipboard(&chr.to_string(), self.conn.clone(), &self.session); + } + } else { + if let Err(e) = handle_key(true, key.clone(), self.conn.clone(), &self.session) { + log::error!("Failed to handle key down: {:?}", e); + } else { + // Only mark modifier as pressed if key-down was actually delivered + self.modifier_state.update(&key, true); + } + // Always mark as released to avoid stuck-modifier state + self.modifier_state.update(&key, false); + if let Err(e) = handle_key(false, key, self.conn.clone(), &self.session) { + log::error!("Failed to handle key up: {:?}", e); + } + } + } + } + + /// Input text via clipboard + Shift+Insert. + /// Shift+Insert is more universal than Ctrl+V, works in both GUI apps and terminals. + /// + /// Note: Clipboard content is NOT restored after paste - see `set_clipboard_for_paste_sync` for rationale. + fn input_text_via_clipboard(text: &str, conn: Arc, session: &Path<'static>) { + if text.is_empty() { + return; + } + if !set_clipboard_for_paste_sync(text) { + return; + } + + let portal = get_portal(&conn); + let shift_keycode = evdev::Key::KEY_LEFTSHIFT.code() as i32; + let insert_keycode = evdev::Key::KEY_INSERT.code() as i32; + + // Send Shift+Insert (universal paste shortcut) + if let Err(e) = remote_desktop_portal::notify_keyboard_keycode( + &portal, + session, + HashMap::new(), + shift_keycode, + PRESSED_DOWN_STATE, + ) { + log::error!("input_text_via_clipboard: failed to press Shift: {:?}", e); + return; + } + + // Press Insert + if let Err(e) = remote_desktop_portal::notify_keyboard_keycode( + &portal, + session, + HashMap::new(), + insert_keycode, + PRESSED_DOWN_STATE, + ) { + log::error!("input_text_via_clipboard: failed to press Insert: {:?}", e); + // Still try to release Shift. + // Note: clipboard has already been set by set_clipboard_for_paste_sync but paste + // never happened. We don't attempt to restore the previous clipboard contents + // because reading the clipboard on Wayland requires focus/permission. + let _ = remote_desktop_portal::notify_keyboard_keycode( + &portal, + session, + HashMap::new(), + shift_keycode, + PRESSED_UP_STATE, + ); + return; + } + + // Release Insert + if let Err(e) = remote_desktop_portal::notify_keyboard_keycode( + &portal, + session, + HashMap::new(), + insert_keycode, + PRESSED_UP_STATE, + ) { + log::error!( + "input_text_via_clipboard: failed to release Insert: {:?}", + e + ); + } + + // Release Shift + if let Err(e) = remote_desktop_portal::notify_keyboard_keycode( + &portal, + session, + HashMap::new(), + shift_keycode, + PRESSED_UP_STATE, + ) { + log::error!("input_text_via_clipboard: failed to release Shift: {:?}", e); } } @@ -196,6 +420,39 @@ pub mod client { } } + /// Send a keysym via RemoteDesktop portal. + fn send_keysym( + keysym: i32, + down: bool, + conn: Arc, + session: &Path<'static>, + ) -> ResultType<()> { + let state: u32 = if down { + PRESSED_DOWN_STATE + } else { + PRESSED_UP_STATE + }; + let portal = get_portal(&conn); + log::trace!( + "send_keysym: calling notify_keyboard_keysym, keysym={:#x}, state={}", + keysym, + state + ); + match remote_desktop_portal::notify_keyboard_keysym( + &portal, + session, + HashMap::new(), + keysym, + state, + ) { + Ok(_) => { + log::trace!("send_keysym: notify_keyboard_keysym succeeded"); + Ok(()) + } + Err(e) => Err(e.into()), + } + } + fn get_raw_evdev_keycode(key: u16) -> i32 { // 8 is the offset between xkb and evdev let mut key = key as i32 - 8; @@ -231,22 +488,86 @@ pub mod client { } _ => { if let Ok((key, is_shift)) = map_key(&key) { - if is_shift { - remote_desktop_portal::notify_keyboard_keycode( + let shift_keycode = evdev::Key::KEY_LEFTSHIFT.code() as i32; + if down { + // Press: Shift down first, then key down + if is_shift { + if let Err(e) = remote_desktop_portal::notify_keyboard_keycode( + &portal, + &session, + HashMap::new(), + shift_keycode, + state, + ) { + log::error!("handle_key: failed to press Shift: {:?}", e); + return Err(e.into()); + } + } + if let Err(e) = remote_desktop_portal::notify_keyboard_keycode( &portal, &session, HashMap::new(), - evdev::Key::KEY_LEFTSHIFT.code() as i32, + key.code() as i32, state, - )?; + ) { + log::error!("handle_key: failed to press key: {:?}", e); + // Best-effort: release Shift if it was pressed + if is_shift { + if let Err(e) = remote_desktop_portal::notify_keyboard_keycode( + &portal, + &session, + HashMap::new(), + shift_keycode, + PRESSED_UP_STATE, + ) { + log::warn!( + "handle_key: best-effort Shift release also failed: {:?}", + e + ); + } + } + return Err(e.into()); + } + } else { + // Release: key up first, then Shift up + if let Err(e) = remote_desktop_portal::notify_keyboard_keycode( + &portal, + &session, + HashMap::new(), + key.code() as i32, + PRESSED_UP_STATE, + ) { + log::error!("handle_key: failed to release key: {:?}", e); + // Best-effort: still try to release Shift + if is_shift { + if let Err(e) = remote_desktop_portal::notify_keyboard_keycode( + &portal, + &session, + HashMap::new(), + shift_keycode, + PRESSED_UP_STATE, + ) { + log::warn!( + "handle_key: best-effort Shift release also failed: {:?}", + e + ); + } + } + return Err(e.into()); + } + if is_shift { + if let Err(e) = remote_desktop_portal::notify_keyboard_keycode( + &portal, + &session, + HashMap::new(), + shift_keycode, + PRESSED_UP_STATE, + ) { + log::error!("handle_key: failed to release Shift: {:?}", e); + return Err(e.into()); + } + } } - remote_desktop_portal::notify_keyboard_keycode( - &portal, - &session, - HashMap::new(), - key.code() as i32, - state, - )?; } } } diff --git a/src/server/uinput.rs b/src/server/uinput.rs index 894ce82f9..a808b4aaa 100644 --- a/src/server/uinput.rs +++ b/src/server/uinput.rs @@ -90,6 +90,13 @@ pub mod client { } fn key_sequence(&mut self, sequence: &str) { + // Sequence events are normally handled in the --server process before reaching here. + // Forward via IPC as a fallback — input_text_wayland can still handle ASCII chars + // via keysym/uinput, though non-ASCII will be skipped (no clipboard in --service). + log::debug!( + "UInputKeyboard::key_sequence called (len={})", + sequence.len() + ); allow_err!(self.send(Data::Keyboard(DataKeyboard::Sequence(sequence.to_string())))); } @@ -178,6 +185,9 @@ pub mod client { pub mod service { use super::*; use hbb_common::lazy_static; + use scrap::wayland::{ + pipewire::RDP_SESSION_INFO, remote_desktop_portal::OrgFreedesktopPortalRemoteDesktop, + }; use std::{collections::HashMap, sync::Mutex}; lazy_static::lazy_static! { @@ -309,6 +319,9 @@ pub mod service { ('/', (evdev::Key::KEY_SLASH, false)), (';', (evdev::Key::KEY_SEMICOLON, false)), ('\'', (evdev::Key::KEY_APOSTROPHE, false)), + // Space is intentionally in both KEY_MAP_LAYOUT (char-to-evdev for text input) + // and KEY_MAP (Key::Space for key events). Both maps serve different lookup paths. + (' ', (evdev::Key::KEY_SPACE, false)), // Shift + key ('A', (evdev::Key::KEY_A, true)), @@ -364,6 +377,155 @@ pub mod service { static ref RESOLUTION: Mutex<((i32, i32), (i32, i32))> = Mutex::new(((0, 0), (0, 0))); } + /// Input text on Wayland using layout-independent methods. + /// ASCII chars (0x20-0x7E): Portal keysym or uinput fallback + /// Non-ASCII chars: skipped — this runs in the --service (root) process where clipboard + /// operations are unreliable (typically no user session environment). + /// Non-ASCII input is normally handled by the --server process via input_text_via_clipboard_server. + fn input_text_wayland(text: &str, keyboard: &mut VirtualDevice) { + let portal_info = { + let session_info = RDP_SESSION_INFO.lock().unwrap(); + session_info + .as_ref() + .map(|info| (info.conn.clone(), info.session.clone())) + }; + + for c in text.chars() { + let keysym = char_to_keysym(c); + if can_input_via_keysym(c, keysym) { + // Try Portal first — down+up on the same channel + if let Some((ref conn, ref session)) = portal_info { + let portal = scrap::wayland::pipewire::get_portal(conn); + if portal + .notify_keyboard_keysym(session, HashMap::new(), keysym, 1) + .is_ok() + { + if let Err(e) = + portal.notify_keyboard_keysym(session, HashMap::new(), keysym, 0) + { + log::warn!( + "input_text_wayland: portal key-up failed for keysym {:#x}: {:?}", + keysym, + e + ); + } + continue; + } + } + // Portal unavailable or failed, fallback to uinput (down+up together) + let key = enigo::Key::Layout(c); + if let Ok((evdev_key, is_shift)) = map_key(&key) { + let mut shift_pressed = false; + if is_shift { + let shift_down = + InputEvent::new(EventType::KEY, evdev::Key::KEY_LEFTSHIFT.code(), 1); + if keyboard.emit(&[shift_down]).is_ok() { + shift_pressed = true; + } else { + log::warn!("input_text_wayland: failed to press Shift for '{}'", c); + } + } + let key_down = InputEvent::new(EventType::KEY, evdev_key.code(), 1); + let key_up = InputEvent::new(EventType::KEY, evdev_key.code(), 0); + allow_err!(keyboard.emit(&[key_down, key_up])); + if shift_pressed { + let shift_up = + InputEvent::new(EventType::KEY, evdev::Key::KEY_LEFTSHIFT.code(), 0); + allow_err!(keyboard.emit(&[shift_up])); + } + } + } else { + log::debug!("Skipping non-ASCII character in uinput service (no clipboard access)"); + } + } + } + + /// Send a single key down or up event for a Layout character. + /// Used by KeyDown/KeyUp to maintain correct press/release semantics. + /// `down`: true for key press, false for key release. + fn input_char_wayland_key_event(chr: char, down: bool, keyboard: &mut VirtualDevice) { + let keysym = char_to_keysym(chr); + let portal_state: u32 = if down { 1 } else { 0 }; + + if can_input_via_keysym(chr, keysym) { + let portal_info = { + let session_info = RDP_SESSION_INFO.lock().unwrap(); + session_info + .as_ref() + .map(|info| (info.conn.clone(), info.session.clone())) + }; + if let Some((ref conn, ref session)) = portal_info { + let portal = scrap::wayland::pipewire::get_portal(conn); + if portal + .notify_keyboard_keysym(session, HashMap::new(), keysym, portal_state) + .is_ok() + { + return; + } + } + // Portal unavailable or failed, fallback to uinput + let key = enigo::Key::Layout(chr); + if let Ok((evdev_key, is_shift)) = map_key(&key) { + if down { + // Press: Shift↓ (if needed) → Key↓ + if is_shift { + let shift_down = + InputEvent::new(EventType::KEY, evdev::Key::KEY_LEFTSHIFT.code(), 1); + if let Err(e) = keyboard.emit(&[shift_down]) { + log::warn!("input_char_wayland_key_event: failed to press Shift for '{}': {:?}", chr, e); + } + } + let key_down = InputEvent::new(EventType::KEY, evdev_key.code(), 1); + allow_err!(keyboard.emit(&[key_down])); + } else { + // Release: Key↑ → Shift↑ (if needed) + let key_up = InputEvent::new(EventType::KEY, evdev_key.code(), 0); + allow_err!(keyboard.emit(&[key_up])); + if is_shift { + let shift_up = + InputEvent::new(EventType::KEY, evdev::Key::KEY_LEFTSHIFT.code(), 0); + if let Err(e) = keyboard.emit(&[shift_up]) { + log::warn!("input_char_wayland_key_event: failed to release Shift for '{}': {:?}", chr, e); + } + } + } + } + } else { + // Non-ASCII: no reliable down/up semantics available. + // Clipboard paste is atomic and handled elsewhere. + log::debug!( + "Skipping non-ASCII character key {} in uinput service", + if down { "down" } else { "up" } + ); + } + } + + /// Check if character can be input via keysym (ASCII printable with valid keysym). + #[inline] + pub(crate) fn can_input_via_keysym(c: char, keysym: i32) -> bool { + // ASCII printable: 0x20 (space) to 0x7E (tilde) + (c as u32 >= 0x20 && c as u32 <= 0x7E) && keysym != 0 + } + + /// Convert a Unicode character to X11 keysym. + pub(crate) fn char_to_keysym(c: char) -> i32 { + let codepoint = c as u32; + if codepoint == 0 { + // Null character has no keysym + 0 + } else if (0x20..=0x7E).contains(&codepoint) { + // ASCII printable (0x20-0x7E): keysym == Unicode codepoint + codepoint as i32 + } else if (0xA0..=0xFF).contains(&codepoint) { + // Latin-1 supplement (0xA0-0xFF): keysym == Unicode codepoint (per X11 keysym spec) + codepoint as i32 + } else { + // Everything else (control chars 0x01-0x1F, DEL 0x7F, and all other non-ASCII Unicode): + // keysym = 0x01000000 | codepoint (X11 Unicode keysym encoding) + (0x0100_0000 | codepoint) as i32 + } + } + fn create_uinput_keyboard() -> ResultType { // TODO: ensure keys here let mut keys = AttributeSet::::new(); @@ -390,13 +552,13 @@ pub mod service { pub fn map_key(key: &enigo::Key) -> ResultType<(evdev::Key, bool)> { if let Some(k) = KEY_MAP.get(&key) { - log::trace!("mapkey {:?}, get {:?}", &key, &k); + log::trace!("mapkey matched in KEY_MAP, evdev={:?}", &k); return Ok((k.clone(), false)); } else { match key { enigo::Key::Layout(c) => { if let Some((k, is_shift)) = KEY_MAP_LAYOUT.get(&c) { - log::trace!("mapkey {:?}, get {:?}", &key, k); + log::trace!("mapkey Layout matched, evdev={:?}", k); return Ok((k.clone(), is_shift.clone())); } } @@ -421,41 +583,68 @@ pub mod service { keyboard: &mut VirtualDevice, data: &DataKeyboard, ) { - log::trace!("handle_keyboard {:?}", &data); + let data_desc = match data { + DataKeyboard::Sequence(seq) => format!("Sequence(len={})", seq.len()), + DataKeyboard::KeyDown(Key::Layout(_)) + | DataKeyboard::KeyUp(Key::Layout(_)) + | DataKeyboard::KeyClick(Key::Layout(_)) => "Layout()".to_string(), + _ => format!("{:?}", data), + }; + log::trace!("handle_keyboard received: {}", data_desc); match data { - DataKeyboard::Sequence(_seq) => { - // ignore + DataKeyboard::Sequence(seq) => { + // Normally handled by --server process (input_text_via_clipboard_server). + // Fallback: input_text_wayland handles ASCII via keysym/uinput; + // non-ASCII will be skipped (no clipboard access in --service process). + if !seq.is_empty() { + input_text_wayland(seq, keyboard); + } } DataKeyboard::KeyDown(enigo::Key::Raw(code)) => { - let down_event = InputEvent::new(EventType::KEY, *code - 8, 1); - allow_err!(keyboard.emit(&[down_event])); - } - DataKeyboard::KeyUp(enigo::Key::Raw(code)) => { - let up_event = InputEvent::new(EventType::KEY, *code - 8, 0); - allow_err!(keyboard.emit(&[up_event])); - } - DataKeyboard::KeyDown(key) => { - if let Ok((k, is_shift)) = map_key(key) { - if is_shift { - let down_event = - InputEvent::new(EventType::KEY, evdev::Key::KEY_LEFTSHIFT.code(), 1); - allow_err!(keyboard.emit(&[down_event])); - } - let down_event = InputEvent::new(EventType::KEY, k.code(), 1); + if *code < 8 { + log::error!("Invalid Raw keycode {} (must be >= 8 due to XKB offset), skipping", code); + } else { + let down_event = InputEvent::new(EventType::KEY, *code - 8, 1); allow_err!(keyboard.emit(&[down_event])); } } - DataKeyboard::KeyUp(key) => { - if let Ok((k, _)) = map_key(key) { - let up_event = InputEvent::new(EventType::KEY, k.code(), 0); + DataKeyboard::KeyUp(enigo::Key::Raw(code)) => { + if *code < 8 { + log::error!("Invalid Raw keycode {} (must be >= 8 due to XKB offset), skipping", code); + } else { + let up_event = InputEvent::new(EventType::KEY, *code - 8, 0); allow_err!(keyboard.emit(&[up_event])); } } + DataKeyboard::KeyDown(key) => { + if let Key::Layout(chr) = key { + input_char_wayland_key_event(*chr, true, keyboard); + } else { + if let Ok((k, _is_shift)) = map_key(key) { + let down_event = InputEvent::new(EventType::KEY, k.code(), 1); + allow_err!(keyboard.emit(&[down_event])); + } + } + } + DataKeyboard::KeyUp(key) => { + if let Key::Layout(chr) = key { + input_char_wayland_key_event(*chr, false, keyboard); + } else { + if let Ok((k, _)) = map_key(key) { + let up_event = InputEvent::new(EventType::KEY, k.code(), 0); + allow_err!(keyboard.emit(&[up_event])); + } + } + } DataKeyboard::KeyClick(key) => { - if let Ok((k, _)) = map_key(key) { - let down_event = InputEvent::new(EventType::KEY, k.code(), 1); - let up_event = InputEvent::new(EventType::KEY, k.code(), 0); - allow_err!(keyboard.emit(&[down_event, up_event])); + if let Key::Layout(chr) = key { + input_text_wayland(&chr.to_string(), keyboard); + } else { + if let Ok((k, _is_shift)) = map_key(key) { + let down_event = InputEvent::new(EventType::KEY, k.code(), 1); + let up_event = InputEvent::new(EventType::KEY, k.code(), 0); + allow_err!(keyboard.emit(&[down_event, up_event])); + } } } DataKeyboard::GetKeyState(key) => { @@ -580,9 +769,13 @@ pub mod service { } fn spawn_keyboard_handler(mut stream: Connection) { + log::debug!("spawn_keyboard_handler: new keyboard handler connection"); tokio::spawn(async move { let mut keyboard = match create_uinput_keyboard() { - Ok(keyboard) => keyboard, + Ok(keyboard) => { + log::debug!("UInput keyboard device created successfully"); + keyboard + } Err(e) => { log::error!("Failed to create keyboard {}", e); return; @@ -602,6 +795,7 @@ pub mod service { handle_keyboard(&mut stream, &mut keyboard, &data).await; } _ => { + log::warn!("Unexpected data type in keyboard handler"); } } } From 9345fb754ac1997a9c9e19578bdc3c933c27e69e Mon Sep 17 00:00:00 2001 From: Nicola Spieser Buiss Date: Tue, 17 Feb 2026 07:29:50 +0100 Subject: [PATCH 133/277] fix: correct typos and improve code clarity (#14341) - Fix 'clipbard' typos in clipboard.rs (function names, comments, strings) - Fix 'seperate' typo in x11/server.rs comment - Replace !is_ok() with idiomatic is_err() in updater.rs - Fix double backtick typo in updater.rs comment Co-authored-by: Ocean --- libs/scrap/src/x11/server.rs | 2 +- src/clipboard.rs | 10 +++++----- src/updater.rs | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/libs/scrap/src/x11/server.rs b/libs/scrap/src/x11/server.rs index f9983f7cf..7ae145d40 100644 --- a/libs/scrap/src/x11/server.rs +++ b/libs/scrap/src/x11/server.rs @@ -98,7 +98,7 @@ unsafe fn check_x11_shm_available(c: *mut xcb_connection_t) -> Result<(), Error> let mut e: *mut xcb_generic_error_t = std::ptr::null_mut(); let reply = xcb_shm_query_version_reply(c, cookie, &mut e as _); if reply.is_null() { - // TODO: Should seperate SHM disabled from SHM not supported? + // TODO: Should separate SHM disabled from SHM not supported? return Err(Error::UnsupportedExtension); } else { // https://github.com/FFmpeg/FFmpeg/blob/6229e4ac425b4566446edefb67d5c225eb397b58/libavdevice/xcbgrab.c#L229 diff --git a/src/clipboard.rs b/src/clipboard.rs index 4280cd124..cae7d03ac 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -197,7 +197,7 @@ pub fn check_clipboard_cm() -> ResultType { #[cfg(not(target_os = "android"))] fn update_clipboard_(multi_clipboards: Vec, side: ClipboardSide) { - let to_update_data = proto::from_multi_clipbards(multi_clipboards); + let to_update_data = proto::from_multi_clipboards(multi_clipboards); if to_update_data.is_empty() { return; } @@ -432,7 +432,7 @@ impl ClipboardContext { #[cfg(target_os = "macos")] let is_kde_x11 = false; let clear_holder_text = if is_kde_x11 { - "RustDesk placeholder to clear the file clipbard" + "RustDesk placeholder to clear the file clipboard" } else { "" } @@ -672,7 +672,7 @@ mod proto { } #[cfg(not(target_os = "android"))] - pub fn from_multi_clipbards(multi_clipboards: Vec) -> Vec { + pub fn from_multi_clipboards(multi_clipboards: Vec) -> Vec { multi_clipboards .into_iter() .filter_map(from_clipboard) @@ -814,7 +814,7 @@ pub mod clipboard_listener { subscribers: listener_lock.subscribers.clone(), }; let (tx_start_res, rx_start_res) = channel(); - let h = start_clipbard_master_thread(handler, tx_start_res); + let h = start_clipboard_master_thread(handler, tx_start_res); let shutdown = match rx_start_res.recv() { Ok((Some(s), _)) => s, Ok((None, err)) => { @@ -854,7 +854,7 @@ pub mod clipboard_listener { log::info!("Clipboard listener unsubscribed: {}", name); } - fn start_clipbard_master_thread( + fn start_clipboard_master_thread( handler: impl ClipboardHandler + Send + 'static, tx_start_res: Sender<(Option, String)>, ) -> JoinHandle<()> { diff --git a/src/updater.rs b/src/updater.rs index e1badd005..c1ff60b46 100644 --- a/src/updater.rs +++ b/src/updater.rs @@ -123,7 +123,7 @@ fn check_update(manually: bool) -> ResultType<()> { if !(manually || config::Config::get_bool_option(config::keys::OPTION_ALLOW_AUTO_UPDATE)) { return Ok(()); } - if !do_check_software_update().is_ok() { + if do_check_software_update().is_err() { // ignore return Ok(()); } @@ -185,7 +185,7 @@ fn check_update(manually: bool) -> ResultType<()> { let mut file = std::fs::File::create(&file_path)?; file.write_all(&file_data)?; } - // We have checked if the `conns`` is empty before, but we need to check again. + // We have checked if the `conns` is empty before, but we need to check again. // No need to care about the downloaded file here, because it's rare case that the `conns` are empty // before the download, but not empty after the download. if has_no_active_conns() { From 20f11018ce086062071ef59f57b5a8dbf88d722f Mon Sep 17 00:00:00 2001 From: cui Date: Thu, 19 Feb 2026 22:24:32 +0800 Subject: [PATCH 134/277] fix: lte should be lt like in linux.rs (#14344) --- src/platform/windows.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index c40e87441..582451240 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -107,9 +107,9 @@ pub fn get_focused_display(displays: Vec) -> Option { let center_x = rect.left + (rect.right - rect.left) / 2; let center_y = rect.top + (rect.bottom - rect.top) / 2; center_x >= display.x - && center_x <= display.x + display.width + && center_x < display.x + display.width && center_y >= display.y - && center_y <= display.y + display.height + && center_y < display.y + display.height }) } } From 34ceeac36e866b7aaae888683cd8b17752ea7a57 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 19 Feb 2026 23:45:06 +0800 Subject: [PATCH 135/277] fix(terminal): fix tabKey parsing for peerIds containing underscores (#14354) Terminal tab keys use the format "peerId_terminalId". The previous code used split('_')[0] or startsWith('$peerId_') to extract the peerId, which breaks when the peerId itself contains underscores. This can happen in two scenarios: - Hostname-based ID: when OPTION_ALLOW_HOSTNAME_AS_ID is enabled, the peerId is derived from the system hostname, which commonly contains underscores (e.g. "my_dev_machine"). - Custom ID: the validation regex ^[a-zA-Z][\w-]{5,15}$ allows underscores since \w matches [a-zA-Z0-9_], so IDs like "my_dev_01" are valid. Fix all three parsing sites in terminal_tab_page.dart to use lastIndexOf('_'), which is safe because terminalId is always a plain integer with no underscores. --- .../lib/desktop/pages/terminal_tab_page.dart | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/flutter/lib/desktop/pages/terminal_tab_page.dart b/flutter/lib/desktop/pages/terminal_tab_page.dart index e06dee321..cd8d84abe 100644 --- a/flutter/lib/desktop/pages/terminal_tab_page.dart +++ b/flutter/lib/desktop/pages/terminal_tab_page.dart @@ -194,7 +194,10 @@ class _TerminalTabPageState extends State { final currentTab = tabController.state.value.selectedTabInfo; assert(call.arguments is String, "Expected String arguments for kWindowEventActiveSession, got ${call.arguments.runtimeType}"); - if (currentTab.key.startsWith(call.arguments)) { + // Use lastIndexOf to handle peerIds containing underscores + final lastUnderscore = currentTab.key.lastIndexOf('_'); + if (lastUnderscore > 0 && + currentTab.key.substring(0, lastUnderscore) == call.arguments) { windowOnTop(windowId()); return true; } @@ -329,7 +332,10 @@ class _TerminalTabPageState extends State { void _addNewTerminal(String peerId, {int? terminalId}) { // Find first tab for this peer to get connection parameters final firstTab = tabController.state.value.tabs.firstWhere( - (tab) => tab.key.startsWith('$peerId\_'), + (tab) { + final last = tab.key.lastIndexOf('_'); + return last > 0 && tab.key.substring(0, last) == peerId; + }, ); if (firstTab.page is TerminalPage) { final page = firstTab.page as TerminalPage; @@ -350,9 +356,10 @@ class _TerminalTabPageState extends State { void _addNewTerminalForCurrentPeer({int? terminalId}) { final currentTab = tabController.state.value.selectedTabInfo; - final parts = currentTab.key.split('_'); - if (parts.isNotEmpty) { - final peerId = parts[0]; + final tabKey = currentTab.key; + final lastUnderscore = tabKey.lastIndexOf('_'); + if (lastUnderscore > 0) { + final peerId = tabKey.substring(0, lastUnderscore); _addNewTerminal(peerId, terminalId: terminalId); } } @@ -369,9 +376,10 @@ class _TerminalTabPageState extends State { labelGetter: DesktopTab.tablabelGetter, tabMenuBuilder: (key) { // Extract peerId from tab key (format: "peerId_terminalId") - final parts = key.split('_'); - if (parts.isEmpty) return Container(); - final peerId = parts[0]; + // Use lastIndexOf to handle peerIds containing underscores + final lastUnderscore = key.lastIndexOf('_'); + if (lastUnderscore <= 0) return Container(); + final peerId = key.substring(0, lastUnderscore); return _tabMenuBuilder(peerId, () {}); }, )); From 483fe80308bbdf8b98ff56430a6519c94200d0fc Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 20 Feb 2026 14:44:25 +0800 Subject: [PATCH 136/277] fix(terminal): fix new tab auto-focus and NaN error on data before layout (#14357) - Fix new tab not auto-focusing: add FocusNode to TerminalView and request focus when tab is selected via tab state listener - Fix NaN error when data arrives before terminal view layout: buffer output data until terminal view has valid dimensions, flush on first valid resize callback Signed-off-by: fufesou --- flutter/lib/desktop/pages/terminal_page.dart | 48 +++++++++++- .../lib/desktop/pages/terminal_tab_page.dart | 1 + flutter/lib/models/terminal_model.dart | 77 +++++++++++++++++-- 3 files changed, 120 insertions(+), 6 deletions(-) diff --git a/flutter/lib/desktop/pages/terminal_page.dart b/flutter/lib/desktop/pages/terminal_page.dart index 17bd86eef..0070cd73b 100644 --- a/flutter/lib/desktop/pages/terminal_page.dart +++ b/flutter/lib/desktop/pages/terminal_page.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; @@ -15,6 +16,7 @@ class TerminalPage extends StatefulWidget { required this.tabController, required this.isSharedPassword, required this.terminalId, + required this.tabKey, this.forceRelay, this.connToken, }) : super(key: key); @@ -25,6 +27,8 @@ class TerminalPage extends StatefulWidget { final bool? isSharedPassword; final String? connToken; final int terminalId; + /// Tab key for focus management, passed from parent to avoid duplicate construction + final String tabKey; final SimpleWrapper?> _lastState = SimpleWrapper(null); FFI get ffi => (_lastState.value! as _TerminalPageState)._ffi; @@ -42,11 +46,16 @@ class _TerminalPageState extends State late FFI _ffi; late TerminalModel _terminalModel; double? _cellHeight; + final FocusNode _terminalFocusNode = FocusNode(canRequestFocus: false); + StreamSubscription? _tabStateSubscription; @override void initState() { super.initState(); + // Listen for tab selection changes to request focus + _tabStateSubscription = widget.tabController.state.listen(_onTabStateChanged); + // Use shared FFI instance from connection manager _ffi = TerminalConnectionManager.getConnection( peerId: widget.id, @@ -64,6 +73,13 @@ class _TerminalPageState extends State _terminalModel.onResizeExternal = (w, h, pw, ph) { _cellHeight = ph * 1.0; + // Enable focus once terminal has valid dimensions (first valid resize) + if (!_terminalFocusNode.canRequestFocus && w > 0 && h > 0) { + _terminalFocusNode.canRequestFocus = true; + // Auto-focus if this tab is currently selected + _requestFocusIfSelected(); + } + // Schedule the setState for the next frame WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { @@ -99,14 +115,42 @@ class _TerminalPageState extends State @override void dispose() { + // Cancel tab state subscription to prevent memory leak + _tabStateSubscription?.cancel(); // Unregister terminal model from FFI _ffi.unregisterTerminalModel(widget.terminalId); _terminalModel.dispose(); + _terminalFocusNode.dispose(); // Release connection reference instead of closing directly TerminalConnectionManager.releaseConnection(widget.id); super.dispose(); } + void _onTabStateChanged(DesktopTabState state) { + // Check if this tab is now selected and request focus + if (state.selected >= 0 && state.selected < state.tabs.length) { + final selectedTab = state.tabs[state.selected]; + if (selectedTab.key == widget.tabKey && mounted) { + _requestFocusIfSelected(); + } + } + } + + void _requestFocusIfSelected() { + if (!mounted || !_terminalFocusNode.canRequestFocus) return; + // Use post-frame callback to ensure widget is fully laid out in focus tree + WidgetsBinding.instance.addPostFrameCallback((_) { + // Re-check conditions after frame: mounted, focusable, still selected, not already focused + if (!mounted || !_terminalFocusNode.canRequestFocus || _terminalFocusNode.hasFocus) return; + final state = widget.tabController.state.value; + if (state.selected >= 0 && state.selected < state.tabs.length) { + if (state.tabs[state.selected].key == widget.tabKey) { + _terminalFocusNode.requestFocus(); + } + } + }); + } + // This method ensures that the number of visible rows is an integer by computing the // extra space left after dividing the available height by the height of a single // terminal row (`_cellHeight`) and distributing it evenly as top and bottom padding. @@ -131,7 +175,9 @@ class _TerminalPageState extends State return TerminalView( _terminalModel.terminal, controller: _terminalModel.terminalController, - autofocus: true, + focusNode: _terminalFocusNode, + // Note: autofocus is not used here because focus is managed manually + // via _onTabStateChanged() to handle tab switching properly. backgroundOpacity: 0.7, padding: _calculatePadding(heightPx), onSecondaryTapDown: (details, offset) async { diff --git a/flutter/lib/desktop/pages/terminal_tab_page.dart b/flutter/lib/desktop/pages/terminal_tab_page.dart index cd8d84abe..a204b8678 100644 --- a/flutter/lib/desktop/pages/terminal_tab_page.dart +++ b/flutter/lib/desktop/pages/terminal_tab_page.dart @@ -92,6 +92,7 @@ class _TerminalTabPageState extends State { key: ValueKey(tabKey), id: peerId, terminalId: terminalId, + tabKey: tabKey, password: password, isSharedPassword: isSharedPassword, tabController: tabController, diff --git a/flutter/lib/models/terminal_model.dart b/flutter/lib/models/terminal_model.dart index ca4f2c11d..764528ab6 100644 --- a/flutter/lib/models/terminal_model.dart +++ b/flutter/lib/models/terminal_model.dart @@ -24,6 +24,13 @@ class TerminalModel with ChangeNotifier { bool _disposed = false; final _inputBuffer = []; + // Buffer for output data received before terminal view has valid dimensions. + // This prevents NaN errors when writing to terminal before layout is complete. + final _pendingOutputChunks = []; + int _pendingOutputSize = 0; + static const int _kMaxOutputBufferChars = 8 * 1024; + // View ready state: true when terminal has valid dimensions, safe to write + bool _terminalViewReady = false; bool get isPeerWindows => parent.ffiModel.pi.platform == kPeerPlatformWindows; @@ -74,6 +81,12 @@ class TerminalModel with ChangeNotifier { // This piece of code must be placed before the conditional check in order to initialize properly. onResizeExternal?.call(w, h, pw, ph); + // Mark terminal view as ready and flush any buffered output on first valid resize. + // Must be after onResizeExternal so the view layer has valid dimensions before flushing. + if (!_terminalViewReady) { + _markViewReady(); + } + if (_terminalOpened) { // Notify remote terminal of resize try { @@ -141,7 +154,7 @@ class TerminalModel with ChangeNotifier { debugPrint('[TerminalModel] Error calling sessionOpenTerminal: $e'); // Optionally show error to user if (e is TimeoutException) { - terminal.write('Failed to open terminal: Connection timeout\r\n'); + _writeToTerminal('Failed to open terminal: Connection timeout\r\n'); } } } @@ -283,7 +296,7 @@ class TerminalModel with ChangeNotifier { })); } } else { - terminal.write('Failed to open terminal: $message\r\n'); + _writeToTerminal('Failed to open terminal: $message\r\n'); } } @@ -327,29 +340,83 @@ class TerminalModel with ChangeNotifier { return; } - terminal.write(text); + _writeToTerminal(text); } catch (e) { debugPrint('[TerminalModel] Failed to process terminal data: $e'); } } } + /// Write text to terminal, buffering if the view is not yet ready. + /// All terminal output should go through this method to avoid NaN errors + /// from writing before the terminal view has valid layout dimensions. + void _writeToTerminal(String text) { + if (!_terminalViewReady) { + // If a single chunk exceeds the cap, keep only its tail. + // Note: truncation may split a multi-byte ANSI escape sequence, + // which can cause a brief visual glitch on flush. This is acceptable + // because it only affects the pre-layout buffering window and the + // terminal will self-correct on subsequent output. + if (text.length >= _kMaxOutputBufferChars) { + final truncated = + text.substring(text.length - _kMaxOutputBufferChars); + _pendingOutputChunks + ..clear() + ..add(truncated); + _pendingOutputSize = truncated.length; + } else { + _pendingOutputChunks.add(text); + _pendingOutputSize += text.length; + // Drop oldest chunks if exceeds limit (whole chunks to preserve ANSI sequences) + while (_pendingOutputSize > _kMaxOutputBufferChars && + _pendingOutputChunks.length > 1) { + final removed = _pendingOutputChunks.removeAt(0); + _pendingOutputSize -= removed.length; + } + } + return; + } + terminal.write(text); + } + + void _flushOutputBuffer() { + if (_pendingOutputChunks.isEmpty) return; + debugPrint( + '[TerminalModel] Flushing $_pendingOutputSize buffered chars (${_pendingOutputChunks.length} chunks)'); + for (final chunk in _pendingOutputChunks) { + terminal.write(chunk); + } + _pendingOutputChunks.clear(); + _pendingOutputSize = 0; + } + + /// Mark terminal view as ready and flush buffered output. + void _markViewReady() { + if (_terminalViewReady) return; + _terminalViewReady = true; + _flushOutputBuffer(); + } + void _handleTerminalClosed(Map evt) { final int exitCode = evt['exit_code'] ?? 0; - terminal.write('\r\nTerminal closed with exit code: $exitCode\r\n'); + _writeToTerminal('\r\nTerminal closed with exit code: $exitCode\r\n'); _terminalOpened = false; notifyListeners(); } void _handleTerminalError(Map evt) { final String message = evt['message'] ?? 'Unknown error'; - terminal.write('\r\nTerminal error: $message\r\n'); + _writeToTerminal('\r\nTerminal error: $message\r\n'); } @override void dispose() { if (_disposed) return; _disposed = true; + // Clear buffers to free memory + _inputBuffer.clear(); + _pendingOutputChunks.clear(); + _pendingOutputSize = 0; // Terminal cleanup is handled server-side when service closes super.dispose(); } From 4d2d2118a2a70985333e6e7580e9670e194bd250 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sat, 21 Feb 2026 11:06:13 +0800 Subject: [PATCH 137/277] Fix/terminal tab close persistent (#14359) * fix(terminal): ensure tab close is resilient to session cleanup failures - Wrap _closeTerminalSessionIfNeeded in isolated try/catch so that tabController.closeBy always executes even if FFI calls throw - Add clarifying comment in handleWindowCloseButton for single-tab audit dialog flow * fix(terminal): fix session reconnect ID mismatch and tab close race condition Remap surviving persistent sessions to client-requested terminal IDs on reconnect, preventing new shell creation when IDs are non-contiguous. Snapshot peerTabCount before async operations in _closeTab to avoid race with concurrent _closeAllTabs clearing the tab controller. Remove debug log statements. Signed-off-by: fufesou --------- Signed-off-by: fufesou --- .../lib/desktop/pages/terminal_tab_page.dart | 193 +++++++++++++++--- src/server/terminal_service.rs | 28 ++- 2 files changed, 186 insertions(+), 35 deletions(-) diff --git a/flutter/lib/desktop/pages/terminal_tab_page.dart b/flutter/lib/desktop/pages/terminal_tab_page.dart index a204b8678..bc3ee1a8c 100644 --- a/flutter/lib/desktop/pages/terminal_tab_page.dart +++ b/flutter/lib/desktop/pages/terminal_tab_page.dart @@ -34,6 +34,8 @@ class _TerminalTabPageState extends State { static const IconData selectedIcon = Icons.terminal; static const IconData unselectedIcon = Icons.terminal_outlined; int _nextTerminalId = 1; + // Lightweight idempotency guard for async close operations + final Set _closingTabs = {}; _TerminalTabPageState(Map params) { Get.put(DesktopTabController(tabType: DesktopTabType.terminal)); @@ -70,24 +72,7 @@ class _TerminalTabPageState extends State { label: tabLabel, selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, - onTabCloseButton: () async { - if (await desktopTryShowTabAuditDialogCloseCancelled( - id: tabKey, - tabController: tabController, - )) { - return; - } - // Close the terminal session first - final ffi = TerminalConnectionManager.getExistingConnection(peerId); - if (ffi != null) { - final terminalModel = ffi.terminalModels[terminalId]; - if (terminalModel != null) { - await terminalModel.closeTerminal(); - } - } - // Then close the tab - tabController.closeBy(tabKey); - }, + onTabCloseButton: () => _closeTab(tabKey), page: TerminalPage( key: ValueKey(tabKey), id: peerId, @@ -102,6 +87,149 @@ class _TerminalTabPageState extends State { ); } + /// Unified tab close handler for all close paths (button, shortcut, programmatic). + /// Shows audit dialog, cleans up session if not persistent, then removes the UI tab. + Future _closeTab(String tabKey) async { + // Idempotency guard: skip if already closing this tab + if (_closingTabs.contains(tabKey)) return; + _closingTabs.add(tabKey); + + try { + // Snapshot peerTabCount BEFORE any await to avoid race with concurrent + // _closeAllTabs clearing tabController (which would make the live count + // drop to 0 and incorrectly trigger session persistence). + // Note: the snapshot may become stale if other individual tabs are closed + // during the audit dialog, but this is an acceptable trade-off. + int? snapshotPeerTabCount; + final parsed = _parseTabKey(tabKey); + if (parsed != null) { + final (peerId, _) = parsed; + snapshotPeerTabCount = tabController.state.value.tabs.where((t) { + final p = _parseTabKey(t.key); + return p != null && p.$1 == peerId; + }).length; + } + + if (await desktopTryShowTabAuditDialogCloseCancelled( + id: tabKey, + tabController: tabController, + )) { + return; + } + + // Close terminal session if not in persistent mode. + // Wrapped separately so session cleanup failure never blocks UI tab removal. + try { + await _closeTerminalSessionIfNeeded(tabKey, + peerTabCount: snapshotPeerTabCount); + } catch (e) { + debugPrint('[TerminalTabPage] Session cleanup failed for $tabKey: $e'); + } + // Always close the tab from UI, regardless of session cleanup result + tabController.closeBy(tabKey); + } catch (e) { + debugPrint('[TerminalTabPage] Error closing tab $tabKey: $e'); + } finally { + _closingTabs.remove(tabKey); + } + } + + /// Close all tabs with session cleanup. + /// Used for window-level close operations (onDestroy, handleWindowCloseButton). + /// UI tabs are removed immediately; session cleanup runs in parallel with a + /// bounded timeout so window close is not blocked indefinitely. + Future _closeAllTabs() async { + final tabKeys = tabController.state.value.tabs.map((t) => t.key).toList(); + // Remove all UI tabs immediately (same instant behavior as the old tabController.clear()) + tabController.clear(); + // Run session cleanup in parallel with bounded timeout (closeTerminal() has internal 3s timeout). + // Skip tabs already being closed by a concurrent _closeTab() to avoid duplicate FFI calls. + final futures = tabKeys + .where((tabKey) => !_closingTabs.contains(tabKey)) + .map((tabKey) async { + try { + await _closeTerminalSessionIfNeeded(tabKey, persistAll: true); + } catch (e) { + debugPrint('[TerminalTabPage] Session cleanup failed for $tabKey: $e'); + } + }).toList(); + if (futures.isNotEmpty) { + await Future.wait(futures).timeout( + const Duration(seconds: 4), + onTimeout: () { + debugPrint( + '[TerminalTabPage] Session cleanup timed out for batch close'); + return []; + }, + ); + } + } + + /// Close the terminal session on server side based on persistent mode. + /// + /// [persistAll] controls behavior when persistent mode is enabled: + /// - `true` (window close): persist all sessions, don't close any. + /// - `false` (tab close): only persist the last session for the peer, + /// close others so only the most recent disconnected session survives. + Future _closeTerminalSessionIfNeeded(String tabKey, + {bool persistAll = false, int? peerTabCount}) async { + final parsed = _parseTabKey(tabKey); + if (parsed == null) return; + final (peerId, terminalId) = parsed; + + final ffi = TerminalConnectionManager.getExistingConnection(peerId); + if (ffi == null) return; + + final isPersistent = bind.sessionGetToggleOptionSync( + sessionId: ffi.sessionId, + arg: kOptionTerminalPersistent, + ); + + if (isPersistent) { + if (persistAll) { + // Window close: persist all sessions + return; + } + // Tab close: only persist if this is the last tab for this peer. + // Use the snapshot value if provided (avoids race with concurrent tab removal). + final effectivePeerTabCount = peerTabCount ?? + tabController.state.value.tabs.where((t) { + final p = _parseTabKey(t.key); + return p != null && p.$1 == peerId; + }).length; + if (effectivePeerTabCount <= 1) { + // Last tab for this peer — persist the session + return; + } + // Not the last tab — fall through to close the session + } + + final terminalModel = ffi.terminalModels[terminalId]; + if (terminalModel != null) { + // closeTerminal() has internal 3s timeout, no need for external timeout + await terminalModel.closeTerminal(); + } + } + + /// Parse tabKey (format: "peerId_terminalId") into its components. + /// Note: peerId may contain underscores, so we use lastIndexOf('_'). + /// Returns null if tabKey format is invalid. + (String peerId, int terminalId)? _parseTabKey(String tabKey) { + final lastUnderscore = tabKey.lastIndexOf('_'); + if (lastUnderscore <= 0) { + debugPrint('[TerminalTabPage] Invalid tabKey format: $tabKey'); + return null; + } + final terminalIdStr = tabKey.substring(lastUnderscore + 1); + final terminalId = int.tryParse(terminalIdStr); + if (terminalId == null) { + debugPrint('[TerminalTabPage] Invalid terminalId in tabKey: $tabKey'); + return null; + } + final peerId = tabKey.substring(0, lastUnderscore); + return (peerId, terminalId); + } + Widget _tabMenuBuilder(String peerId, CancelFunc cancelFunc) { final List> menu = []; const EdgeInsets padding = EdgeInsets.only(left: 8.0, right: 5.0); @@ -185,7 +313,8 @@ class _TerminalTabPageState extends State { } else if (call.method == kWindowEventRestoreTerminalSessions) { _restoreSessions(call.arguments); } else if (call.method == "onDestroy") { - tabController.clear(); + // Clean up sessions before window destruction (bounded wait) + await _closeAllTabs(); } else if (call.method == kWindowActionRebuild) { reloadCurrentWindow(); } else if (call.method == kWindowEventActiveSession) { @@ -269,7 +398,7 @@ class _TerminalTabPageState extends State { // macOS: Cmd+W (standard for close tab) final currentTab = tabController.state.value.selectedTabInfo; if (tabController.state.value.tabs.length > 1) { - tabController.closeBy(currentTab.key); + _closeTab(currentTab.key); return true; } } else if (!isMacOS && @@ -278,7 +407,7 @@ class _TerminalTabPageState extends State { // Other platforms: Ctrl+Shift+W (to avoid conflict with Ctrl+W word delete) final currentTab = tabController.state.value.selectedTabInfo; if (tabController.state.value.tabs.length > 1) { - tabController.closeBy(currentTab.key); + _closeTab(currentTab.key); return true; } } @@ -357,12 +486,10 @@ class _TerminalTabPageState extends State { void _addNewTerminalForCurrentPeer({int? terminalId}) { final currentTab = tabController.state.value.selectedTabInfo; - final tabKey = currentTab.key; - final lastUnderscore = tabKey.lastIndexOf('_'); - if (lastUnderscore > 0) { - final peerId = tabKey.substring(0, lastUnderscore); - _addNewTerminal(peerId, terminalId: terminalId); - } + final parsed = _parseTabKey(currentTab.key); + if (parsed == null) return; + final (peerId, _) = parsed; + _addNewTerminal(peerId, terminalId: terminalId); } @override @@ -376,11 +503,9 @@ class _TerminalTabPageState extends State { selectedBorderColor: MyTheme.accent, labelGetter: DesktopTab.tablabelGetter, tabMenuBuilder: (key) { - // Extract peerId from tab key (format: "peerId_terminalId") - // Use lastIndexOf to handle peerIds containing underscores - final lastUnderscore = key.lastIndexOf('_'); - if (lastUnderscore <= 0) return Container(); - final peerId = key.substring(0, lastUnderscore); + final parsed = _parseTabKey(key); + if (parsed == null) return Container(); + final (peerId, _) = parsed; return _tabMenuBuilder(peerId, () {}); }, )); @@ -435,7 +560,7 @@ class _TerminalTabPageState extends State { } } if (connLength <= 1) { - tabController.clear(); + await _closeAllTabs(); return true; } else { final bool res; @@ -446,7 +571,7 @@ class _TerminalTabPageState extends State { res = await closeConfirmDialog(); } if (res) { - tabController.clear(); + await _closeAllTabs(); } return res; } diff --git a/src/server/terminal_service.rs b/src/server/terminal_service.rs index 743f849c4..ed7d02f68 100644 --- a/src/server/terminal_service.rs +++ b/src/server/terminal_service.rs @@ -777,6 +777,32 @@ impl TerminalServiceProxy { ) -> Result> { let mut response = TerminalResponse::new(); + // When the client requests a terminal_id that doesn't exist but there are + // surviving persistent sessions, remap the lowest-ID session to the requested + // terminal_id. This handles the case where _nextTerminalId resets to 1 on + // reconnect but the server-side sessions have non-contiguous IDs (e.g. {2: htop}). + // + // The client's requested terminal_id may not match any surviving session ID + // (e.g. _nextTerminalId incremented beyond the surviving IDs). This remap is a + // one-time handle reassignment — only the first reconnect triggers it because + // needs_session_sync is cleared afterward. Remaining sessions are communicated + // back via `persistent_sessions` with their original server-side IDs. + if !service.sessions.contains_key(&open.terminal_id) + && service.needs_session_sync + && !service.sessions.is_empty() + { + if let Some(&lowest_id) = service.sessions.keys().min() { + log::info!( + "Remapping persistent session {} -> {} for reconnection", + lowest_id, + open.terminal_id + ); + if let Some(session_arc) = service.sessions.remove(&lowest_id) { + service.sessions.insert(open.terminal_id, session_arc); + } + } + } + // Check if terminal already exists if let Some(session_arc) = service.sessions.get(&open.terminal_id) { // Reconnect to existing terminal @@ -824,7 +850,7 @@ impl TerminalServiceProxy { // Create new terminal session log::info!( - "Creating new terminal {} for service: {}", + "Creating new terminal {} for service {}", open.terminal_id, service.service_id ); From 6c3515588f8eb99e5e94462c93993b7577bf6939 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Sun, 22 Feb 2026 14:59:25 +0800 Subject: [PATCH 138/277] - UI display: display_name first (#14358) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * - UI display: display_name first - Fallback: name - Technical identity: still name ### What changed - Added account display helpers and display_name state in user model: - flutter/lib/models/user_model.dart:16 - Account/logout label now uses display_name (@name) when both exist: - flutter/lib/mobile/pages/settings_page.dart:689 - flutter/lib/desktop/pages/desktop_setting_page.dart:2016 - flutter/lib/desktop/pages/desktop_setting_page.dart:2135 - Desktop Account info now shows both when applicable: - Display Name: ... - Username: ... - flutter/lib/desktop/pages/desktop_setting_page.dart:2039 - Previously done group-list behavior remains: - group user list displays display_name with name fallback - flutter/lib/common/widgets/my_group.dart:187 - Persistence path for display_name remains enabled (including group cache/submodule field): - libs/hbb_common/src/config.rs:2347 - src/client.rs:2630 - LoginRequest.my_name now resolves as: 1. OPTION_DISPLAY_NAME (manual override) 2. user_info.display_name 3. user_info.name 4. OS username fallback * 1. GUID key (...Uninstall\{GUID}) is MSI-native metadata generated by Windows Installer. 2. Non-GUID key (...Uninstall\RustDesk) is explicitly written by RustDesk’s MSI compatibility component in res/msi/Package/Components/Regs.wxs:44, populated by preprocess.py --arp from .github/workflows/ flutter-build.yml:262. So they were not using the same EstimatedSize logic: - MSI GUID key: MSI-calculated size (KB). - RustDesk key: custom script value from res/msi/preprocess.py:339 (previously bytes, now fixed to KB). That mismatch is exactly why you saw different sizes. * improve display name handling - Append (@username) when multiple users share the same display name - Trim whitespace from display_name before comparison and display - Add missing translate() for Logout button on desktop Signed-off-by: 21pages * group peer filter match both user's display name and user's name Signed-off-by: 21pages * case-insensitive search in group peer filter Signed-off-by: 21pages --------- Signed-off-by: 21pages Co-authored-by: 21pages --- flutter/lib/common/hbbs/hbbs.dart | 8 +++++ flutter/lib/common/widgets/my_group.dart | 29 ++++++++++++++----- flutter/lib/common/widgets/peers_view.dart | 13 +++++---- .../desktop/pages/desktop_setting_page.dart | 12 ++++++-- flutter/lib/mobile/pages/settings_page.dart | 2 +- flutter/lib/models/user_model.dart | 19 +++++++++++- libs/hbb_common | 2 +- res/msi/preprocess.py | 4 ++- src/client.rs | 9 ++++-- src/hbbs_http/account.rs | 9 +++++- src/lang/ar.rs | 1 + src/lang/be.rs | 1 + src/lang/bg.rs | 1 + src/lang/ca.rs | 1 + src/lang/cn.rs | 1 + src/lang/cs.rs | 1 + src/lang/da.rs | 1 + src/lang/de.rs | 1 + src/lang/el.rs | 1 + src/lang/eo.rs | 1 + src/lang/es.rs | 1 + src/lang/et.rs | 1 + src/lang/eu.rs | 1 + src/lang/fa.rs | 1 + src/lang/fi.rs | 1 + src/lang/fr.rs | 1 + src/lang/ge.rs | 1 + src/lang/he.rs | 1 + src/lang/hr.rs | 1 + src/lang/hu.rs | 1 + src/lang/id.rs | 1 + src/lang/it.rs | 1 + src/lang/ja.rs | 1 + src/lang/ko.rs | 1 + src/lang/kz.rs | 1 + src/lang/lt.rs | 1 + src/lang/lv.rs | 1 + src/lang/nb.rs | 1 + src/lang/nl.rs | 1 + src/lang/pl.rs | 1 + src/lang/pt_PT.rs | 1 + src/lang/ptbr.rs | 1 + src/lang/ro.rs | 1 + src/lang/ru.rs | 1 + src/lang/sc.rs | 1 + src/lang/sk.rs | 1 + src/lang/sl.rs | 1 + src/lang/sq.rs | 1 + src/lang/sr.rs | 1 + src/lang/sv.rs | 1 + src/lang/ta.rs | 1 + src/lang/template.rs | 1 + src/lang/th.rs | 1 + src/lang/tr.rs | 1 + src/lang/tw.rs | 1 + src/lang/uk.rs | 1 + src/lang/vi.rs | 1 + src/ui/index.tis | 25 ++++++++++++++-- 58 files changed, 153 insertions(+), 26 deletions(-) diff --git a/flutter/lib/common/hbbs/hbbs.dart b/flutter/lib/common/hbbs/hbbs.dart index aab8ba597..f3b210184 100644 --- a/flutter/lib/common/hbbs/hbbs.dart +++ b/flutter/lib/common/hbbs/hbbs.dart @@ -25,6 +25,7 @@ enum UserStatus { kDisabled, kNormal, kUnverified } // Is all the fields of the user needed? class UserPayload { String name = ''; + String displayName = ''; String email = ''; String note = ''; String? verifier; @@ -33,6 +34,7 @@ class UserPayload { UserPayload.fromJson(Map json) : name = json['name'] ?? '', + displayName = json['display_name'] ?? '', email = json['email'] ?? '', note = json['note'] ?? '', verifier = json['verifier'], @@ -46,6 +48,7 @@ class UserPayload { Map toJson() { final Map map = { 'name': name, + 'display_name': displayName, 'status': status == UserStatus.kDisabled ? 0 : status == UserStatus.kUnverified @@ -58,9 +61,14 @@ class UserPayload { Map toGroupCacheJson() { final Map map = { 'name': name, + 'display_name': displayName, }; return map; } + + String get displayNameOrName { + return displayName.trim().isEmpty ? name : displayName; + } } class PeerPayload { diff --git a/flutter/lib/common/widgets/my_group.dart b/flutter/lib/common/widgets/my_group.dart index 6207a7363..74ce34e71 100644 --- a/flutter/lib/common/widgets/my_group.dart +++ b/flutter/lib/common/widgets/my_group.dart @@ -158,12 +158,18 @@ class _MyGroupState extends State { return Obx(() { final userItems = gFFI.groupModel.users.where((p0) { if (searchAccessibleItemNameText.isNotEmpty) { - return p0.name - .toLowerCase() - .contains(searchAccessibleItemNameText.value.toLowerCase()); + final search = searchAccessibleItemNameText.value.toLowerCase(); + return p0.name.toLowerCase().contains(search) || + p0.displayNameOrName.toLowerCase().contains(search); } return true; }).toList(); + // Count occurrences of each displayNameOrName to detect duplicates + final displayNameCount = {}; + for (final u in userItems) { + final dn = u.displayNameOrName; + displayNameCount[dn] = (displayNameCount[dn] ?? 0) + 1; + } final deviceGroupItems = gFFI.groupModel.deviceGroups.where((p0) { if (searchAccessibleItemNameText.isNotEmpty) { return p0.name @@ -177,7 +183,8 @@ class _MyGroupState extends State { itemCount: deviceGroupItems.length + userItems.length, itemBuilder: (context, index) => index < deviceGroupItems.length ? _buildDeviceGroupItem(deviceGroupItems[index]) - : _buildUserItem(userItems[index - deviceGroupItems.length])); + : _buildUserItem(userItems[index - deviceGroupItems.length], + displayNameCount)); var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0); return Obx(() => stateGlobal.isPortrait.isFalse ? listView(false) @@ -185,8 +192,14 @@ class _MyGroupState extends State { }); } - Widget _buildUserItem(UserPayload user) { + Widget _buildUserItem(UserPayload user, Map displayNameCount) { final username = user.name; + final dn = user.displayNameOrName; + final isDuplicate = (displayNameCount[dn] ?? 0) > 1; + final displayName = + isDuplicate && user.displayName.trim().isNotEmpty + ? '${user.displayName} (@$username)' + : dn; return InkWell(onTap: () { isSelectedDeviceGroup.value = false; if (selectedAccessibleItemName.value != username) { @@ -222,14 +235,14 @@ class _MyGroupState extends State { alignment: Alignment.center, child: Center( child: Text( - username.characters.first.toUpperCase(), + displayName.characters.first.toUpperCase(), style: TextStyle(color: Colors.white), textAlign: TextAlign.center, ), ), ), ).marginOnly(right: 4), - if (isMe) Flexible(child: Text(username)), + if (isMe) Flexible(child: Text(displayName)), if (isMe) Flexible( child: Container( @@ -246,7 +259,7 @@ class _MyGroupState extends State { ), ), ), - if (!isMe) Expanded(child: Text(username)), + if (!isMe) Expanded(child: Text(displayName)), ], ).paddingSymmetric(vertical: 4), ), diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart index d81a095ca..5be5af272 100644 --- a/flutter/lib/common/widgets/peers_view.dart +++ b/flutter/lib/common/widgets/peers_view.dart @@ -570,11 +570,14 @@ class MyGroupPeerView extends BasePeersView { static bool filter(Peer peer) { final model = gFFI.groupModel; if (model.searchAccessibleItemNameText.isNotEmpty) { - final text = model.searchAccessibleItemNameText.value; - final searchPeersOfUser = peer.loginName.contains(text) && - model.users.any((user) => user.name == peer.loginName); - final searchPeersOfDeviceGroup = peer.device_group_name.contains(text) && - model.deviceGroups.any((g) => g.name == peer.device_group_name); + final text = model.searchAccessibleItemNameText.value.toLowerCase(); + final searchPeersOfUser = model.users.any((user) => + user.name == peer.loginName && + (user.name.toLowerCase().contains(text) || + user.displayNameOrName.toLowerCase().contains(text))); + final searchPeersOfDeviceGroup = + peer.device_group_name.toLowerCase().contains(text) && + model.deviceGroups.any((g) => g.name == peer.device_group_name); if (!searchPeersOfUser && !searchPeersOfDeviceGroup) { return false; } diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index b26d909cb..3314d82ab 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -2016,7 +2016,9 @@ class _AccountState extends State<_Account> { Widget accountAction() { return Obx(() => _Button( - gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout', + gFFI.userModel.userName.value.isEmpty + ? 'Login' + : '${translate('Logout')} (${gFFI.userModel.accountLabelWithHandle})', () => { gFFI.userModel.userName.value.isEmpty ? loginDialog() @@ -2037,6 +2039,10 @@ class _AccountState extends State<_Account> { offstage: gFFI.userModel.userName.value.isEmpty, child: Column( children: [ + if (gFFI.userModel.displayName.value.trim().isNotEmpty && + gFFI.userModel.displayName.value.trim() != + gFFI.userModel.userName.value.trim()) + text('Display Name', gFFI.userModel.displayName.value.trim()), text('Username', gFFI.userModel.userName.value), // text('Group', gFFI.groupModel.groupName.value), ], @@ -2130,7 +2136,9 @@ class _PluginState extends State<_Plugin> { Widget accountAction() { return Obx(() => _Button( - gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout', + gFFI.userModel.userName.value.isEmpty + ? 'Login' + : '${translate('Logout')} (${gFFI.userModel.accountLabelWithHandle})', () => { gFFI.userModel.userName.value.isEmpty ? loginDialog() diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index c2e2ef57d..afd3422d7 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -688,7 +688,7 @@ class _SettingsState extends State with WidgetsBindingObserver { SettingsTile( title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty ? translate('Login') - : '${translate('Logout')} (${gFFI.userModel.userName.value})')), + : '${translate('Logout')} (${gFFI.userModel.accountLabelWithHandle})')), leading: Icon(Icons.person), onPressed: (context) { if (gFFI.userModel.userName.value.isEmpty) { diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart index 217d74aee..c850c4cf6 100644 --- a/flutter/lib/models/user_model.dart +++ b/flutter/lib/models/user_model.dart @@ -16,9 +16,23 @@ bool refreshingUser = false; class UserModel { final RxString userName = ''.obs; + final RxString displayName = ''.obs; final RxBool isAdmin = false.obs; final RxString networkError = ''.obs; bool get isLogin => userName.isNotEmpty; + String get displayNameOrUserName => + displayName.value.trim().isEmpty ? userName.value : displayName.value; + String get accountLabelWithHandle { + final username = userName.value.trim(); + if (username.isEmpty) { + return ''; + } + final preferred = displayName.value.trim(); + if (preferred.isEmpty || preferred == username) { + return username; + } + return '$preferred (@$username)'; + } WeakReference parent; UserModel(this.parent) { @@ -98,7 +112,8 @@ class UserModel { _updateLocalUserInfo() { final userInfo = getLocalUserInfo(); if (userInfo != null) { - userName.value = userInfo['name']; + userName.value = (userInfo['name'] ?? '').toString(); + displayName.value = (userInfo['display_name'] ?? '').toString(); } } @@ -110,10 +125,12 @@ class UserModel { await gFFI.groupModel.reset(); } userName.value = ''; + displayName.value = ''; } _parseAndUpdateUser(UserPayload user) { userName.value = user.name; + displayName.value = user.displayName; isAdmin.value = user.isAdmin; bind.mainSetLocalOption(key: 'user_info', value: jsonEncode(user)); if (isWeb) { diff --git a/libs/hbb_common b/libs/hbb_common index da339dca6..0b60b9ffa 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit da339dca64ecae3273838c0a1395c7fe2f1a1016 +Subproject commit 0b60b9ffa05259f72cd33e79010ef8e15d42b851 diff --git a/res/msi/preprocess.py b/res/msi/preprocess.py index cb2140d21..c590549f4 100644 --- a/res/msi/preprocess.py +++ b/res/msi/preprocess.py @@ -336,7 +336,9 @@ def gen_custom_ARPSYSTEMCOMPONENT_True(args, dist_dir): f'{indent}\n' ) - estimated_size = get_folder_size(dist_dir) + # EstimatedSize in uninstall registry must be in KB. + estimated_size_bytes = get_folder_size(dist_dir) + estimated_size = max(1, (estimated_size_bytes + 1023) // 1024) lines_new.append( f'{indent}\n' ) diff --git a/src/client.rs b/src/client.rs index a7b681ee1..cb4ed3a24 100644 --- a/src/client.rs +++ b/src/client.rs @@ -2630,10 +2630,13 @@ impl LoginConfigHandler { display_name = serde_json::from_str::(&LocalConfig::get_option("user_info")) .map(|x| { - x.get("name") - .map(|x| x.as_str().unwrap_or_default()) + x.get("display_name") + .and_then(|x| x.as_str()) + .map(|x| x.trim()) + .filter(|x| !x.is_empty()) + .or_else(|| x.get("name").and_then(|x| x.as_str())) + .map(|x| x.to_owned()) .unwrap_or_default() - .to_owned() }) .unwrap_or_default(); } diff --git a/src/hbbs_http/account.rs b/src/hbbs_http/account.rs index 6bdef6f06..6644aee28 100644 --- a/src/hbbs_http/account.rs +++ b/src/hbbs_http/account.rs @@ -80,6 +80,8 @@ pub enum UserStatus { pub struct UserPayload { pub name: String, #[serde(default)] + pub display_name: Option, + #[serde(default)] pub email: Option, #[serde(default)] pub note: Option, @@ -268,7 +270,12 @@ impl OidcSession { ); LocalConfig::set_option( "user_info".to_owned(), - serde_json::json!({ "name": auth_body.user.name, "status": auth_body.user.status }).to_string(), + serde_json::json!({ + "name": auth_body.user.name, + "display_name": auth_body.user.display_name, + "status": auth_body.user.status + }) + .to_string(), ); } } diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 65853847a..fc1f79c38 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "متابعة مع {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 0b8492e9c..a7656782d 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Працягнуць з {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 986b7b1fb..3036e31b2 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Продължи с {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 3a7d5498e..05a7e7899 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Continua amb {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 516015390..5cb228a6e 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "使用 {} 登录"), + ("Display Name", "显示名称"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 497af5cf1..944ee4b95 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Pokračovat s {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 6505f2bdf..8140fcaec 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Fortsæt med {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 5ada5b270..a518dd3c3 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", "Bildschirm während ausgehender Sitzungen aktiv halten"), ("keep-awake-during-incoming-sessions-label", "Bildschirm während eingehender Sitzungen aktiv halten"), ("Continue with {}", "Fortfahren mit {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 1542a8ee1..8b02c3c89 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Συνέχεια με {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 303fc45a8..3d6b6924f 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", ""), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index bceff6a56..8ad0c4cab 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Continuar con {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index 4d87490ac..def665ec5 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Jätka koos {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index ba0979fe7..2454dcb8a 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "{} honekin jarraitu"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 5fe019444..52be56c81 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "ادامه با {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fi.rs b/src/lang/fi.rs index 59f25538b..0d9b42ddd 100644 --- a/src/lang/fi.rs +++ b/src/lang/fi.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Jatka käyttäen {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 9637233aa..1d54448c9 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", "Maintenir l’écran allumé lors des sessions sortantes"), ("keep-awake-during-incoming-sessions-label", "Maintenir l’écran allumé lors des sessions entrantes"), ("Continue with {}", "Continuer avec {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ge.rs b/src/lang/ge.rs index ffb9e351d..10b5e7f27 100644 --- a/src/lang/ge.rs +++ b/src/lang/ge.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "{}-ით გაგრძელება"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 74b93c155..00999708f 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "המשך עם {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 8232b8635..d00fc56b9 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Nastavi sa {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index c9f5453b9..174cdb28b 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", "Képernyő aktív állapotban tartása a kimenő munkamenetek során"), ("keep-awake-during-incoming-sessions-label", "Képernyő aktív állapotban tartása a bejövő munkamenetek során"), ("Continue with {}", "Folytatás a következővel: {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index f7498dd99..f898c8bc4 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Lanjutkan dengan {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index eabfac559..28edb0e8a 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", "Mantieni lo schermo attivo durante le sessioni in uscita"), ("keep-awake-during-incoming-sessions-label", "Mantieni lo schermo attivo durante le sessioni in ingresso"), ("Continue with {}", "Continua con {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index c89899469..e033de3b3 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "{} で続行"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index d860af5ab..1e3d4f9b8 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", "발신 세션 중 화면 켜짐 유지"), ("keep-awake-during-incoming-sessions-label", "수신 세션 중 화면 켜짐 유지"), ("Continue with {}", "{}(으)로 계속"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index eaa0bb34d..c3715672d 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", ""), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 18080ee77..91c76291a 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Tęsti su {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 12b90d8f1..0c8ba694e 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Turpināt ar {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index b118a4b7c..9c38fcbb8 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Fortsett med {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index f952a844e..577f7487f 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", "Houd het scherm open tijdens de uitgaande sessies."), ("keep-awake-during-incoming-sessions-label", "Houd het scherm open tijdens de inkomende sessies."), ("Continue with {}", "Ga verder met {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 6d2185e47..000c05921 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", "Utrzymuj urządzenie w stanie aktywnym podczas sesji wychodzących"), ("keep-awake-during-incoming-sessions-label", "Utrzymuj urządzenie w stanie aktywnym podczas sesji przychodzących"), ("Continue with {}", "Kontynuuj z {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 6a3e49817..ccbdd574e 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", ""), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index e16f7ba61..a7a2f7db6 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", "Manter tela ativa durante sessões de saída"), ("keep-awake-during-incoming-sessions-label", "Manter tela ativa durante sessões de entrada"), ("Continue with {}", "Continuar com {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 9c21617d7..8917b2a46 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Continuă cu {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index f4ae05e99..344260d34 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", "Не отключать экран во время исходящих сеансов"), ("keep-awake-during-incoming-sessions-label", "Не отключать экран во время входящих сеансов"), ("Continue with {}", "Продолжить с {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index 46c4c582e..2eef86908 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Sighi cun {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 85cd17594..0b45d7e12 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Pokračovať s {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 9c7dead43..d8e22a3c4 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Nadaljuj z {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index b4f4fb694..b7b7321ab 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Vazhdo me {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index a12fc3311..46cb14cdd 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Nastavi sa {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index f85e88853..d2d1a3911 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Fortsätt med {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs index 4f545f055..7e3ae5cd0 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "{} உடன் தொடர்"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index c9aec1a3e..b21f64f14 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", ""), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 6d66b44fd..dbfc1096c 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "ทำต่อด้วย {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 319b631cd..ac8b3d368 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", "Giden oturumlar süresince ekranı açık tutun"), ("keep-awake-during-incoming-sessions-label", "Gelen oturumlar süresince ekranı açık tutun"), ("Continue with {}", "{} ile devam et"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index b66567e43..0e01fcde5 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", "在連出工作階段期間保持螢幕喚醒"), ("keep-awake-during-incoming-sessions-label", "在連入工作階段期間保持螢幕喚醒"), ("Continue with {}", "使用 {} 登入"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index bf95a02f7..b49b2e5ae 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Продовжити з {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vi.rs b/src/lang/vi.rs index 1e64c6234..8f5888509 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -739,5 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", ""), ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Tiếp tục với {}"), + ("Display Name", ""), ].iter().cloned().collect(); } diff --git a/src/ui/index.tis b/src/ui/index.tis index 09aa0c306..d4934ba0b 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -358,6 +358,22 @@ function getUserName() { return ''; } +function getAccountLabelWithHandle() { + try { + var user = JSON.parse(handler.get_local_option("user_info")); + var username = (user.name || '').trim(); + if (!username) { + return ''; + } + var displayName = (user.display_name || '').trim(); + if (!displayName || displayName == username) { + return username; + } + return displayName + " (@" + username + ")"; + } catch(e) {} + return ''; +} + // Shared dialog functions function open_custom_server_dialog() { var configOptions = handler.get_options(); @@ -493,7 +509,7 @@ class MyIdMenu: Reactor.Component { } function renderPop() { - var username = handler.get_local_option("access_token") ? getUserName() : ''; + var accountLabel = handler.get_local_option("access_token") ? getAccountLabelWithHandle() : ''; return {!disable_settings &&
  • {svg_checkmark}{translate('Enable keyboard/mouse')}
  • } @@ -521,8 +537,8 @@ class MyIdMenu: Reactor.Component { {!disable_settings && } {!disable_settings && false && handler.using_public_server() &&
  • {svg_checkmark}{translate('Always connect via relay')}
  • } {!disable_change_id && handler.is_ok_change_id() ?
    : ""} - {!disable_account && (username ? -
  • {translate('Logout')} ({username})
  • : + {!disable_account && (accountLabel ? +
  • {translate('Logout')} ({accountLabel})
  • :
  • {translate('Login')}
  • )} {!disable_change_id && !disable_settings && handler.is_ok_change_id() && key_confirmed && connect_status > 0 ?
  • {translate('Change ID')}
  • : ""}
    @@ -1430,6 +1446,9 @@ checkConnectStatus(); function set_local_user_info(user) { var user_info = {name: user.name}; + if (user.display_name) { + user_info.display_name = user.display_name; + } if (user.status) { user_info.status = user.status; } From 17a3f2ae52929fc63bfa3d8b92811632f3619c3f Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Mon, 23 Feb 2026 09:37:53 +0100 Subject: [PATCH 139/277] Italian language update (#14375) --- src/lang/it.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 28edb0e8a..aac87109d 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -739,6 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", "Mantieni lo schermo attivo durante le sessioni in uscita"), ("keep-awake-during-incoming-sessions-label", "Mantieni lo schermo attivo durante le sessioni in ingresso"), ("Continue with {}", "Continua con {}"), - ("Display Name", ""), + ("Display Name", "Visualizza nome"), ].iter().cloned().collect(); } From 8a889d3ebb6915df0320659e4bb88131af45b338 Mon Sep 17 00:00:00 2001 From: westor Date: Tue, 24 Feb 2026 10:29:43 +0200 Subject: [PATCH 140/277] Update el.rs translation (#14378) - Added missing language strings. - Fixed some previously typo translations. - Updated some translation strings. --- src/lang/el.rs | 440 ++++++++++++++++++++++++------------------------- 1 file changed, 220 insertions(+), 220 deletions(-) diff --git a/src/lang/el.rs b/src/lang/el.rs index 8b02c3c89..8812f7d04 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -3,7 +3,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Κατάσταση"), ("Your Desktop", "Ο σταθμός εργασίας σας"), - ("desk_tip", "Η πρόσβαση στον σταθμό εργασίας σας είναι δυνατή με αυτό το αναγνωριστικό και τον κωδικό πρόσβασης."), + ("desk_tip", "Η πρόσβαση στον σταθμό εργασίας σας είναι δυνατή με αυτό το ID και τον κωδικό πρόσβασης."), ("Password", "Κωδικός πρόσβασης"), ("Ready", "Έτοιμο"), ("Established", "Συνδέθηκε"), @@ -19,16 +19,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recent sessions", "Πρόσφατες συνεδρίες"), ("Address book", "Βιβλίο διευθύνσεων"), ("Confirmation", "Επιβεβαίωση"), - ("TCP tunneling", "TCP tunneling"), + ("TCP tunneling", "Σήραγγα TCP"), ("Remove", "Κατάργηση"), - ("Refresh random password", "Νέος τυχαίος κωδικός πρόσβασης"), + ("Refresh random password", "Ανανέωση τυχαίου κωδικού πρόσβασης"), ("Set your own password", "Ορίστε τον δικό σας κωδικό πρόσβασης"), ("Enable keyboard/mouse", "Ενεργοποίηση πληκτρολογίου/ποντικιού"), ("Enable clipboard", "Ενεργοποίηση προχείρου"), ("Enable file transfer", "Ενεργοποίηση μεταφοράς αρχείων"), - ("Enable TCP tunneling", "Ενεργοποίηση TCP tunneling"), + ("Enable TCP tunneling", "Ενεργοποίηση σήραγγας TCP"), ("IP Whitelisting", "Λίστα επιτρεπόμενων IP"), - ("ID/Relay Server", "Διακομιστής ID/Αναμετάδοσης"), + ("ID/Relay Server", "ID/Διακομιστής Αναμετάδοσης"), ("Import server config", "Εισαγωγή διαμόρφωσης διακομιστή"), ("Export Server Config", "Εξαγωγή διαμόρφωσης διακομιστή"), ("Import server configuration successfully", "Επιτυχής εισαγωγή διαμόρφωσης διακομιστή"), @@ -36,14 +36,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Invalid server configuration", "Μη έγκυρη διαμόρφωση διακομιστή"), ("Clipboard is empty", "Το πρόχειρο είναι κενό"), ("Stop service", "Διακοπή υπηρεσίας"), - ("Change ID", "Αλλαγή αναγνωριστικού ID"), + ("Change ID", "Αλλαγή του ID σας"), ("Your new ID", "Το νέο σας ID"), ("length %min% to %max%", "μέγεθος από %min% έως %max%"), ("starts with a letter", "ξεκινά με γράμμα"), ("allowed characters", "επιτρεπόμενοι χαρακτήρες"), ("id_change_tip", "Επιτρέπονται μόνο οι χαρακτήρες a-z, A-Z, 0-9, - (παύλα) και _ (κάτω παύλα). Το πρώτο γράμμα πρέπει να είναι a-z, A-Z και το μήκος πρέπει να είναι μεταξύ 6 και 16 χαρακτήρων."), ("Website", "Ιστότοπος"), - ("About", "Πληροφορίες"), + ("About", "Σχετικά"), ("Slogan_tip", "Φτιαγμένο με πάθος - σε έναν κόσμο που βυθίζεται στο χάος!"), ("Privacy Statement", "Πολιτική απορρήτου"), ("Mute", "Σίγαση"), @@ -53,7 +53,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Audio Input", "Είσοδος ήχου"), ("Enhancements", "Βελτιώσεις"), ("Hardware Codec", "Κωδικοποιητής υλικού"), - ("Adaptive bitrate", "Adaptive bitrate"), + ("Adaptive bitrate", "Προσαρμοστικός ρυθμός μετάδοσης bit"), ("ID Server", "Διακομιστής ID"), ("Relay Server", "Διακομιστής αναμετάδοσης"), ("API Server", "Διακομιστής API"), @@ -67,18 +67,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Skip", "Παράλειψη"), ("Close", "Κλείσιμο"), ("Retry", "Δοκίμασε ξανά"), - ("OK", "ΟΚ"), + ("OK", "Εντάξει"), ("Password Required", "Απαιτείται κωδικός πρόσβασης"), ("Please enter your password", "Παρακαλώ εισάγετε τον κωδικό πρόσβασης"), ("Remember password", "Απομνημόνευση κωδικού πρόσβασης"), ("Wrong Password", "Λάθος κωδικός πρόσβασης"), - ("Do you want to enter again?", "Επανασύνδεση;"), + ("Do you want to enter again?", "Θέλετε να γίνει επανασύνδεση;"), ("Connection Error", "Σφάλμα σύνδεσης"), ("Error", "Σφάλμα"), ("Reset by the peer", "Η σύνδεση επαναφέρθηκε από τον απομακρυσμένο σταθμό"), ("Connecting...", "Σύνδεση..."), ("Connection in progress. Please wait.", "Σύνδεση σε εξέλιξη. Παρακαλώ περιμένετε."), - ("Please try 1 minute later", "Παρακαλώ ξαναδοκιμάστε σε 1 λεπτό"), + ("Please try 1 minute later", "Παρακαλώ δοκιμάστε ξανά σε 1 λεπτό"), ("Login Error", "Σφάλμα εισόδου"), ("Successful", "Επιτυχής"), ("Connected, waiting for image...", "Συνδέθηκε, αναμονή για εικόνα..."), @@ -101,10 +101,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select All", "Επιλογή όλων"), ("Unselect All", "Κατάργηση επιλογής όλων"), ("Empty Directory", "Κενός φάκελος"), - ("Not an empty directory", "Ο φάκελος δεν είναι κενός"), + ("Not an empty directory", "Η διαδρομή δεν είναι κενή"), ("Are you sure you want to delete this file?", "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το αρχείο;"), - ("Are you sure you want to delete this empty directory?", "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτόν τον κενό φάκελο;"), - ("Are you sure you want to delete the file of this directory?", "Είστε βέβαιοι ότι θέλετε να διαγράψετε το αρχείο αυτού του φακέλου;"), + ("Are you sure you want to delete this empty directory?", "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτήν την κενή διαδρομή;"), + ("Are you sure you want to delete the file of this directory?", "Είστε βέβαιοι ότι θέλετε να διαγράψετε το αρχείο αυτής της διαδρομής;"), ("Do this for all conflicts", "Κάνε αυτό για όλες τις διενέξεις"), ("This is irreversible!", "Αυτό είναι μη αναστρέψιμο!"), ("Deleting", "Διαγραφή"), @@ -133,8 +133,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insert Ctrl + Alt + Del", "Εισαγωγή Ctrl + Alt + Del"), ("Insert Lock", "Κλείδωμα απομακρυσμένου σταθμού"), ("Refresh", "Ανανέωση"), - ("ID does not exist", "Το αναγνωριστικό ID δεν υπάρχει"), - ("Failed to connect to rendezvous server", "Αποτυχία σύνδεσης με διακομιστή"), + ("ID does not exist", "Το ID αυτό δεν υπάρχει"), + ("Failed to connect to rendezvous server", "Αποτυχία σύνδεσης με τον διακομιστή"), ("Please try later", "Παρακαλώ δοκιμάστε αργότερα"), ("Remote desktop is offline", "Ο απομακρυσμένος σταθμός εργασίας είναι εκτός σύνδεσης"), ("Key mismatch", "Μη έγκυρο κλειδί"), @@ -146,17 +146,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set Password", "Ορίστε κωδικό πρόσβασης"), ("OS Password", "Κωδικός πρόσβασης λειτουργικού συστήματος"), ("install_tip", "Λόγω UAC, το RustDesk ενδέχεται να μην λειτουργεί σωστά σε ορισμένες περιπτώσεις. Για να αποφύγετε το UAC, κάντε κλικ στο κουμπί παρακάτω για να εγκαταστήσετε το RustDesk στο σύστημα"), - ("Click to upgrade", "Αναβάθμιση τώρα"), + ("Click to upgrade", "Κάντε κλίκ για αναβάθμιση τώρα"), ("Configure", "Διαμόρφωση"), - ("config_acc", "Για τον απομακρυσμένο έλεγχο του υπολογιστή σας, πρέπει να εκχωρήσετε δικαιώματα πρόσβασης στο RustDesk."), - ("config_screen", "Για να αποκτήσετε απομακρυσμένη πρόσβαση στον υπολογιστή σας, πρέπει να εκχωρήσετε το δικαίωμα RustDesk \"Screen Capture\"."), + ("config_acc", "Για να ελέγξετε την επιφάνεια εργασίας σας από απόσταση, πρέπει να παραχωρήσετε στο RustDesk το δικαίωμα της \"Προσβασιμότητας\"."), + ("config_screen", "Για να αποκτήσετε απομακρυσμένη πρόσβαση στην επιφάνεια εργασίας σας, πρέπει να παραχωρήσετε στο RustDesk το δικαίωμα της \"Εγγραφή οθόνης\"."), ("Installing ...", "Γίνεται εγκατάσταση ..."), ("Install", "Εγκατάσταση"), ("Installation", "Η εγκατάσταση"), ("Installation Path", "Διαδρομή εγκατάστασης"), ("Create start menu shortcuts", "Δημιουργία συντομεύσεων μενού έναρξης"), ("Create desktop icon", "Δημιουργία εικονιδίου επιφάνειας εργασίας"), - ("agreement_tip", "Με την εγκατάσταση αποδέχεστε την άδεια χρήσης"), + ("agreement_tip", "Με την εγκατάσταση, αποδέχεστε την άδεια χρήσης"), ("Accept and Install", "Αποδοχή και εγκατάσταση"), ("End-user license agreement", "Σύμβαση άδειας χρήσης τελικού χρήστη"), ("Generating ...", "Δημιουργία ..."), @@ -170,8 +170,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local Port", "Τοπική θύρα"), ("Local Address", "Τοπική διεύθυνση"), ("Change Local Port", "Αλλαγή τοπικής θύρας"), - ("setup_server_tip", "Για πιο γρήγορη σύνδεση, ρυθμίστε τον δικό σας διακομιστή σύνδεσης"), - ("Too short, at least 6 characters.", "Πολύ μικρό, τουλάχιστον 6 χαρακτήρες."), + ("setup_server_tip", "Για πιο γρήγορη σύνδεση, παρακαλούμε να ρυθμίστε τον δικό σας διακομιστή σύνδεσης"), + ("Too short, at least 6 characters.", "Πολύ μικρό, χρειάζεται τουλάχιστον 6 χαρακτήρες."), ("The confirmation is not identical.", "Η επιβεβαίωση δεν είναι πανομοιότυπη."), ("Permissions", "Άδειες"), ("Accept", "Αποδοχή"), @@ -183,7 +183,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relayed and encrypted connection", "Κρυπτογραφημένη σύνδεση με αναμετάδοση"), ("Direct and unencrypted connection", "Άμεση και μη κρυπτογραφημένη σύνδεση"), ("Relayed and unencrypted connection", "Μη κρυπτογραφημένη σύνδεση με αναμετάδοση"), - ("Enter Remote ID", "Εισαγωγή απομακρυσμένου ID"), + ("Enter Remote ID", "Εισαγωγή του απομακρυσμένου ID"), ("Enter your password", "Εισάγετε τον κωδικό σας"), ("Logging in...", "Γίνεται σύνδεση..."), ("Enable RDP session sharing", "Ενεργοποίηση κοινής χρήσης RDP"), @@ -200,35 +200,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Login screen using Wayland is not supported", "Η οθόνη εισόδου με χρήση του Wayland δεν υποστηρίζεται"), ("Reboot required", "Απαιτείται επανεκκίνηση"), ("Unsupported display server", "Μη υποστηριζόμενος διακομιστής εμφάνισης "), - ("x11 expected", "απαιτείται X11"), + ("x11 expected", "αναμένεται X11"), ("Port", "Θύρα"), ("Settings", "Ρυθμίσεις"), ("Username", "Όνομα χρήστη"), ("Invalid port", "Μη έγκυρη θύρα"), - ("Closed manually by the peer", "Έκλεισε από τον απομακρυσμένο σταθμό"), - ("Enable remote configuration modification", "Ενεργοποίηση απομακρυσμένης τροποποίησης ρυθμίσεων"), + ("Closed manually by the peer", "Τερματίστηκε από τον απομακρυσμένο σταθμό"), + ("Enable remote configuration modification", "Ενεργοποίηση απομακρυσμένης τροποποίησης διαμόρφωσης"), ("Run without install", "Εκτέλεση χωρίς εγκατάσταση"), - ("Connect via relay", "Πραγματοποίηση σύνδεση μέσω αναμεταδότη"), - ("Always connect via relay", "Σύνδεση πάντα μέσω αναμεταδότη"), - ("whitelist_tip", "Μόνο οι IP της λίστας επιτρεπόμενων έχουν πρόσβαση"), + ("Connect via relay", "Σύνδεση μέσω αναμεταδότη"), + ("Always connect via relay", "Να γίνεται σύνδεση πάντα μέσω αναμεταδότη"), + ("whitelist_tip", "Μόνο οι IP της λίστας επιτρεπόμενων να έχουν πρόσβαση σε εμένα"), ("Login", "Σύνδεση"), ("Verify", "Επαλήθευση"), ("Remember me", "Να με θυμάσαι"), - ("Trust this device", "Εμπιστεύομαι αυτή την συσκευή"), + ("Trust this device", "Να εμπιστεύομαι αυτή την συσκευή"), ("Verification code", "Κωδικός επαλήθευσης"), - ("verification_tip", "Εντοπίστηκε νέα συσκευή και εστάλη ένας κωδικός επαλήθευσης στην καταχωρισμένη διεύθυνση email. Εισαγάγετε τον κωδικό επαλήθευσης για να συνδεθείτε ξανά."), + ("verification_tip", "Ένας κωδικός επαλήθευσης έχει σταλεί στην καταχωρημένη διεύθυνση email. Εισαγάγετε τον κωδικό επαλήθευσης για να συνεχίσετε τη σύνδεση."), ("Logout", "Αποσύνδεση"), ("Tags", "Ετικέτες"), ("Search ID", "Αναζήτηση ID"), - ("whitelist_sep", "Διαχωρίζονται με κόμμα, ερωτηματικό, διάστημα ή νέα γραμμή"), - ("Add ID", "Προσθήκη αναγνωριστικού ID"), + ("whitelist_sep", "Διαχωρίζονται με κόμμα, ερωτηματικό, κενό ή νέα γραμμή"), + ("Add ID", "Προσθήκη ID"), ("Add Tag", "Προσθήκη ετικέτας"), - ("Unselect all tags", "Κατάργηση επιλογής όλων των ετικετών"), + ("Unselect all tags", "Αποεπιλογή όλων των ετικετών"), ("Network error", "Σφάλμα δικτύου"), ("Username missed", "Δεν συμπληρώσατε το όνομα χρήστη"), ("Password missed", "Δεν συμπληρώσατε τον κωδικό πρόσβασης"), ("Wrong credentials", "Λάθος διαπιστευτήρια"), - ("The verification code is incorrect or has expired", ""), + ("The verification code is incorrect or has expired", "Ο κωδικός επαλήθευσης είναι λανθασμένος ή έχει λήξει"), ("Edit Tag", "Επεξεργασία ετικέτας"), ("Forget Password", "Διαγραφή απομνημονευμένου κωδικού"), ("Favorites", "Αγαπημένα"), @@ -239,7 +239,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Socks5 Proxy", "Διαμεσολαβητής Socks5"), ("Socks5/Http(s) Proxy", "Διαμεσολαβητής Socks5/Http(s)"), ("Discovered", "Ανακαλύφθηκαν"), - ("install_daemon_tip", "Για να ξεκινά με την εκκίνηση του υπολογιστή, πρέπει να εγκαταστήσετε την υπηρεσία συστήματος"), + ("install_daemon_tip", "Για να ξεκινά με την εκκίνηση του υπολογιστή, πρέπει να εγκαταστήσετε την υπηρεσία συστήματος."), ("Remote ID", "Απομακρυσμένο ID"), ("Paste", "Επικόλληση"), ("Paste here?", "Επικόλληση εδώ;"), @@ -262,28 +262,28 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Pinch to Zoom", "Τσίμπημα για ζουμ"), ("Canvas Zoom", "Ζουμ σε καμβά"), ("Reset canvas", "Επαναφορά καμβά"), - ("No permission of file transfer", "Δεν υπάρχει άδεια για μεταφορά αρχείων"), + ("No permission of file transfer", "Δεν υπάρχει άδεια για την μεταφορά αρχείων"), ("Note", "Σημείωση"), ("Connection", "Σύνδεση"), ("Share screen", "Κοινή χρήση οθόνης"), ("Chat", "Κουβέντα"), ("Total", "Σύνολο"), ("items", "στοιχεία"), - ("Selected", "Επιλεγμένο"), - ("Screen Capture", "Αποτύπωση οθόνης"), + ("Selected", "Επιλεγμένα"), + ("Screen Capture", "Καταγραφή οθόνης"), ("Input Control", "Έλεγχος εισόδου"), ("Audio Capture", "Εγγραφή ήχου"), ("Do you accept?", "Δέχεσαι;"), ("Open System Setting", "Άνοιγμα ρυθμίσεων συστήματος"), - ("How to get Android input permission?", "Πώς να αποκτήσω άδεια εισαγωγής Android;"), + ("How to get Android input permission?", "Πώς να αποκτήσω άδεια εισόδου για Android;"), ("android_input_permission_tip1", "Για να μπορεί μία απομακρυσμένη συσκευή να ελέγχει τη συσκευή σας Android, πρέπει να επιτρέψετε στο RustDesk να χρησιμοποιεί την υπηρεσία \"Προσβασιμότητα\"."), - ("android_input_permission_tip2", "Παρακαλώ μεταβείτε στην επόμενη σελίδα ρυθμίσεων συστήματος, βρείτε και πληκτρολογήστε [Εγκατεστημένες υπηρεσίες], ενεργοποιήστε την υπηρεσία [Είσοδος RustDesk]."), - ("android_new_connection_tip", "θέλω να ελέγξω τη συσκευή σου."), - ("android_service_will_start_tip", "Η ενεργοποίηση της κοινής χρήσης οθόνης θα ξεκινήσει αυτόματα την υπηρεσία, ώστε άλλες συσκευές να μπορούν να ελέγχουν αυτήν τη συσκευή Android."), - ("android_stop_service_tip", "Η απενεργοποίηση της υπηρεσίας θα αποσυνδέσει αυτόματα όλες τις εγκατεστημένες συνδέσεις."), - ("android_version_audio_tip", "Η έκδοση Android που διαθέτετε δεν υποστηρίζει εγγραφή ήχου, ενημερώστε το σε Android 10 ή νεότερη έκδοση, εάν είναι δυνατόν."), - ("android_start_service_tip", ""), - ("android_permission_may_not_change_tip", ""), + ("android_input_permission_tip2", "Παρακαλούμε να μεταβείτε στην επόμενη σελίδα ρυθμίσεων συστήματος, βρείτε και πληκτρολογήστε [Εγκατεστημένες υπηρεσίες], ενεργοποιήστε την υπηρεσία [Είσοδος RustDesk]."), + ("android_new_connection_tip", "Έχει ληφθεί νέο αίτημα ελέγχου, το οποίο θέλει να ελέγξει την τρέχουσα συσκευή σας."), + ("android_service_will_start_tip", "Η ενεργοποίηση της \"Καταγραφής οθόνης\" θα ξεκινήσει αυτόματα την υπηρεσία, επιτρέποντας σε άλλες συσκευές να ζητήσουν σύνδεση με τη συσκευή σας."), + ("android_stop_service_tip", "Το κλείσιμο της υπηρεσίας αυτής θα κλείσει αυτόματα όλες τις υπάρχουσες συνδέσεις."), + ("android_version_audio_tip", "Η τρέχουσα έκδοση Android δεν υποστηρίζει εγγραφή ήχου, αναβαθμίστε σε Android 10 ή νεότερη έκδοση."), + ("android_start_service_tip", "Πατήστε [Έναρξη υπηρεσίας] ή ενεργοποιήστε την άδεια [Καταγραφή οθόνης] για να ξεκινήσετε την υπηρεσία κοινής χρήσης οθόνης."), + ("android_permission_may_not_change_tip", "Τα δικαιώματα για τις καθιερωμένες συνδέσεις δεν μπορούν να αλλάξουν άμεσα μέχρι να επανασυνδεθούν."), ("Account", "Λογαριασμός"), ("Overwrite", "Αντικατάσταση"), ("This file exists, skip or overwrite this file?", "Αυτό το αρχείο υπάρχει, παράβλεψη ή αντικατάσταση αυτού του αρχείου;"), @@ -293,14 +293,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Succeeded", "Επιτυχής"), ("Someone turns on privacy mode, exit", "Κάποιος ενεργοποιεί τη λειτουργία απορρήτου, έξοδος"), ("Unsupported", "Δεν υποστηρίζεται"), - ("Peer denied", "Ο απομακρυσμένος σταθμός απέρριψε τη σύνδεση"), + ("Peer denied", "Ο απομακρυσμένος σταθμός έχει απορριφθεί"), ("Please install plugins", "Παρακαλώ εγκαταστήστε τα πρόσθετα"), ("Peer exit", "Ο απομακρυσμένος σταθμός έχει αποσυνδεθεί"), ("Failed to turn off", "Αποτυχία απενεργοποίησης"), ("Turned off", "Απενεργοποιημένο"), ("Language", "Γλώσσα"), - ("Keep RustDesk background service", "Εκτέλεση του RustDesk στο παρασκήνιο"), - ("Ignore Battery Optimizations", "Παράβλεψη βελτιστοποιήσεων μπαταρίας"), + ("Keep RustDesk background service", "Διατήρηση της υπηρεσίας παρασκηνίου του RustDesk"), + ("Ignore Battery Optimizations", "Αγνόηση βελτιστοποιήσεων μπαταρίας"), ("android_open_battery_optimizations_tip", "Θέλετε να ανοίξετε τις ρυθμίσεις βελτιστοποίησης μπαταρίας;"), ("Start on boot", "Έναρξη κατά την εκκίνηση"), ("Start the screen sharing service on boot, requires special permissions", "Η έναρξη της υπηρεσίας κοινής χρήσης οθόνης κατά την εκκίνηση, απαιτεί ειδικά δικαιώματα"), @@ -315,11 +315,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restart remote device", "Επανεκκίνηση απομακρυσμένης συσκευής"), ("Are you sure you want to restart", "Είστε βέβαιοι ότι θέλετε να κάνετε επανεκκίνηση"), ("Restarting remote device", "Γίνεται επανεκκίνηση της απομακρυσμένης συσκευής"), - ("remote_restarting_tip", "Η απομακρυσμένη συσκευή επανεκκινείται, κλείστε αυτό το μήνυμα και επανασυνδεθείτε χρησιμοποιώντας τον μόνιμο κωδικό πρόσβασης."), + ("remote_restarting_tip", "Γίνεται επανεκκίνηση της απομακρυσμένης συσκευής. Κλείστε αυτό το πλαίσιο μηνύματος και επανασυνδεθείτε με τον μόνιμο κωδικό πρόσβασης μετά από λίγο."), ("Copied", "Αντιγράφηκε"), ("Exit Fullscreen", "Έξοδος από πλήρη οθόνη"), ("Fullscreen", "Πλήρης οθόνη"), - ("Mobile Actions", "Mobile Actions"), + ("Mobile Actions", "Ενέργειες για κινητά"), ("Select Monitor", "Επιλογή οθόνης"), ("Control Actions", "Ενέργειες ελέγχου"), ("Display Settings", "Ρυθμίσεις οθόνης"), @@ -347,7 +347,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable audio", "Ενεργοποίηση ήχου"), ("Unlock Network Settings", "Ξεκλείδωμα ρυθμίσεων δικτύου"), ("Server", "Διακομιστής"), - ("Direct IP Access", "Πρόσβαση με χρήση IP"), + ("Direct IP Access", "Άμεση πρόσβαση IP"), ("Proxy", "Διαμεσολαβητής"), ("Apply", "Εφαρμογή"), ("Disconnect all devices?", "Αποσύνδεση όλων των συσκευών;"), @@ -358,7 +358,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Pin Toolbar", "Καρφίτσωμα γραμμής εργαλείων"), ("Unpin Toolbar", "Ξεκαρφίτσωμα γραμμής εργαλείων"), ("Recording", "Εγγραφή"), - ("Directory", "Φάκελος εγγραφών"), + ("Directory", "Διαδρομή"), ("Automatically record incoming sessions", "Αυτόματη εγγραφή εισερχόμενων συνεδριών"), ("Automatically record outgoing sessions", "Αυτόματη εγγραφή εξερχόμενων συνεδριών"), ("Change", "Αλλαγή"), @@ -373,23 +373,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_foreground_window_tip", "Το τρέχον παράθυρο της απομακρυσμένης επιφάνειας εργασίας απαιτεί υψηλότερα δικαιώματα για να λειτουργήσει, επομένως δεν μπορεί να χρησιμοποιήσει προσωρινά το ποντίκι και το πληκτρολόγιο. Μπορείτε να ζητήσετε από τον απομακρυσμένο χρήστη να ελαχιστοποιήσει το τρέχον παράθυρο ή να κάνετε κλικ στο κουμπί ανύψωσης στο παράθυρο διαχείρισης σύνδεσης. Για να αποφύγετε αυτό το πρόβλημα, συνιστάται η εγκατάσταση του λογισμικού στην απομακρυσμένη συσκευή."), ("Disconnected", "Αποσυνδέθηκε"), ("Other", "Άλλα"), - ("Confirm before closing multiple tabs", "Επιβεβαίωση πριν κλείσετε πολλές καρτέλες"), + ("Confirm before closing multiple tabs", "Επιβεβαίωση πριν κλείσουν πολλαπλές καρτέλες"), ("Keyboard Settings", "Ρυθμίσεις πληκτρολογίου"), ("Full Access", "Πλήρης πρόσβαση"), ("Screen Share", "Κοινή χρήση οθόνης"), ("Wayland requires Ubuntu 21.04 or higher version.", "Το Wayland απαιτεί Ubuntu 21.04 ή νεότερη έκδοση."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Το Wayland απαιτεί υψηλότερη έκδοση του linux distro. Δοκιμάστε την επιφάνεια εργασίας X11 ή αλλάξτε το λειτουργικό σας σύστημα."), - ("JumpLink", "Προβολή"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Το Wayland απαιτεί υψηλότερη έκδοση διανομής του linux. Δοκιμάστε την επιφάνεια εργασίας X11 ή αλλάξτε το λειτουργικό σας σύστημα."), + ("JumpLink", "Σύνδεσμος μετάβασης"), ("Please Select the screen to be shared(Operate on the peer side).", "Επιλέξτε την οθόνη που θέλετε να μοιραστείτε (Λειτουργία στην πλευρά του απομακρυσμένου σταθμού)."), - ("Show RustDesk", "Εμφάνιση RustDesk"), + ("Show RustDesk", "Εμφάνιση του RustDesk"), ("This PC", "Αυτός ο υπολογιστής"), ("or", "ή"), ("Elevate", "Ανύψωση"), - ("Zoom cursor", "Kέρσορας μεγέθυνσης"), + ("Zoom cursor", "Δρομέας ζουμ"), ("Accept sessions via password", "Αποδοχή συνεδριών με κωδικό πρόσβασης"), ("Accept sessions via click", "Αποδοχή συνεδριών με κλικ"), ("Accept sessions via both", "Αποδοχή συνεδριών και με τα δύο"), - ("Please wait for the remote side to accept your session request...", "Παρακαλώ περιμένετε μέχρι η απομακρυσμένη πλευρά να αποδεχτεί το αίτημα συνεδρίας σας..."), + ("Please wait for the remote side to accept your session request...", "Παρακαλώ περιμένετε μέχρι η απομακρυσμένη πλευρά να αποδεχτεί το αίτημα της συνεδρίας σας..."), ("One-time Password", "Κωδικός μίας χρήσης"), ("Use one-time password", "Χρήση κωδικού πρόσβασης μίας χρήσης"), ("One-time password length", "Μήκος κωδικού πρόσβασης μίας χρήσης"), @@ -398,27 +398,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Να επιτρέπεται η απόκρυψη, μόνο εάν αποδέχεστε συνδέσεις μέσω κωδικού πρόσβασης και χρησιμοποιείτε μόνιμο κωδικό πρόσβασης"), ("wayland_experiment_tip", "Η υποστήριξη Wayland βρίσκεται σε πειραματικό στάδιο, χρησιμοποιήστε το X11 εάν χρειάζεστε πρόσβαση χωρίς επίβλεψη."), ("Right click to select tabs", "Κάντε δεξί κλικ για να επιλέξετε καρτέλες"), - ("Skipped", "Παράλειψη"), - ("Add to address book", "Προσθήκη στο Βιβλίο Διευθύνσεων"), + ("Skipped", "Παραλήφθηκε"), + ("Add to address book", "Προσθήκη στο βιβλίο διευθύνσεων"), ("Group", "Ομάδα"), ("Search", "Αναζήτηση"), - ("Closed manually by web console", "Κλειστό χειροκίνητα από την κονσόλα web"), + ("Closed manually by web console", "Κλείσιμο χειροκίνητα από την κονσόλα ιστού"), ("Local keyboard type", "Τύπος τοπικού πληκτρολογίου"), ("Select local keyboard type", "Επιλογή τύπου τοπικού πληκτρολογίου"), - ("software_render_tip", "Εάν έχετε κάρτα γραφικών Nvidia και το παράθυρο σύνδεσης κλείνει αμέσως μετά τη σύνδεση, η εγκατάσταση του προγράμματος οδήγησης nouveau και η επιλογή χρήσης της επιτάχυνσης γραφικών μέσω λογισμικού μπορεί να βοηθήσει. Απαιτείται επανεκκίνηση."), - ("Always use software rendering", "Επιτάχυνση γραφικών μέσω λογισμικού"), - ("config_input", "Για να ελέγξετε την απομακρυσμένη επιφάνεια εργασίας με πληκτρολόγιο, πρέπει να εκχωρήσετε δικαιώματα στο RustDesk"), - ("config_microphone", "Ρύθμιση μικροφώνου"), - ("request_elevation_tip", "αίτημα ανύψωσης δικαιωμάτων χρήστη"), + ("software_render_tip", "Εάν χρησιμοποιείτε κάρτα γραφικών της Nvidia σε Linux και το παράθυρο απομακρυσμένης πρόσβασης κλείνει αμέσως μετά τη σύνδεση, η μετάβαση στο πρόγραμμα οδήγησης της Nouveau ανοιχτού κώδικα και η επιλογή χρήσης απόδοσης λογισμικού μπορεί να βοηθήσει. Απαιτείται επανεκκίνηση του λογισμικού."), + ("Always use software rendering", "Να χρησιμοποιείτε πάντα η απόδοση λογισμικού"), + ("config_input", "Για να ελέγξετε την απομακρυσμένη επιφάνεια εργασίας με το πληκτρολόγιο, πρέπει να παραχωρήσετε στο RustDesk το δικαίωμα της \"Παρακολούθηση εισόδου\"."), + ("config_microphone", "Για να μιλήσετε εξ αποστάσεως, πρέπει να παραχωρήσετε στο RustDesk το δικαίωμα της \"Εγγραφή ήχου\"."), + ("request_elevation_tip", "Μπορείτε επίσης να ζητήσετε ανύψωση εάν υπάρχει κάποιος στην απομακρυσμένη πλευρά."), ("Wait", "Περιμένετε"), - ("Elevation Error", "Σφάλμα ανύψωσης δικαιωμάτων χρήστη"), + ("Elevation Error", "Σφάλμα ανύψωσης"), ("Ask the remote user for authentication", "Ζητήστε από τον απομακρυσμένο χρήστη έλεγχο ταυτότητας"), ("Choose this if the remote account is administrator", "Επιλέξτε αυτό εάν ο απομακρυσμένος λογαριασμός είναι διαχειριστής"), - ("Transmit the username and password of administrator", "Αποστολή του ονόματος χρήστη και του κωδικού πρόσβασης του διαχειριστή"), - ("still_click_uac_tip", "Εξακολουθεί να απαιτεί από τον απομακρυσμένο χρήστη να κάνει κλικ στο OK στο παράθυρο UAC όπου εκτελείται το RustDesk."), - ("Request Elevation", "Αίτημα ανύψωσης δικαιωμάτων χρήστη"), - ("wait_accept_uac_tip", "Περιμένετε να αποδεχτεί ο απομακρυσμένος χρήστης το παράθυρο διαλόγου UAC."), - ("Elevate successfully", "Επιτυχής ανύψωση δικαιωμάτων χρήστη"), + ("Transmit the username and password of administrator", "Μεταδώστε το όνομα χρήστη και τον κωδικό πρόσβασης του διαχειριστή"), + ("still_click_uac_tip", "Εξακολουθεί να απαιτεί από τον απομακρυσμένο χρήστη να κάνει κλικ στο πλήκτρο Εντάξει στο παράθυρο UAC όπου εκτελείται το RustDesk."), + ("Request Elevation", "Αίτημα ανύψωσης"), + ("wait_accept_uac_tip", "Περιμένετε μέχρι ο απομακρυσμένος χρήστης να αποδεχτεί το παράθυρο διαλόγου UAC."), + ("Elevate successfully", "Επιτυχής ανύψωση"), ("uppercase", "κεφαλαία γράμματα"), ("lowercase", "πεζά γράμματα"), ("digit", "αριθμός"), @@ -427,7 +427,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Weak", "Αδύναμο"), ("Medium", "Μέτριο"), ("Strong", "Δυνατό"), - ("Switch Sides", "Εναλλαγή πλευράς"), + ("Switch Sides", "Αλλαγή πλευρών"), ("Please confirm if you want to share your desktop?", "Παρακαλώ επιβεβαιώστε αν επιθυμείτε την κοινή χρήση της επιφάνειας εργασίας;"), ("Display", "Εμφάνιση"), ("Default View Style", "Προκαθορισμένος τρόπος εμφάνισης"), @@ -441,11 +441,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Φωνητική κλήση"), ("Text chat", "Συνομιλία κειμένου"), ("Stop voice call", "Διακοπή φωνητικής κλήσης"), - ("relay_hint_tip", "Εάν δεν είναι δυνατή η απευθείας σύνδεση, μπορείτε να δοκιμάσετε να συνδεθείτε μέσω διακομιστή αναμετάδοσης"), + ("relay_hint_tip", "Ενδέχεται να μην είναι δυνατή η απευθείας σύνδεση: μπορείτε να δοκιμάσετε να συνδεθείτε μέσω αναμετάδοσης. Επιπλέον, εάν θέλετε να χρησιμοποιήσετε την αναμετάδοση στην πρώτη σας προσπάθεια, μπορείτε να προσθέσετε την \"/r\" κατάληξη στο ID ή να επιλέξετε την επιλογή \"Πάντα σύνδεση μέσω αναμετάδοσης\" στην κάρτα πρόσφατων συνεδριών, εάν υπάρχει."), ("Reconnect", "Επανασύνδεση"), ("Codec", "Κωδικοποίηση"), ("Resolution", "Ανάλυση"), - ("No transfers in progress", "Δεν υπάρχει μεταφορά σε εξέλιξη"), + ("No transfers in progress", "Δεν υπάρχουν μεταφορές σε εξέλιξη"), ("Set one-time password length", "Μέγεθος κωδικού μιας χρήσης"), ("RDP Settings", "Ρυθμίσεις RDP"), ("Sort by", "Ταξινόμηση κατά"), @@ -454,35 +454,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Minimize", "Ελαχιστοποίηση"), ("Maximize", "Μεγιστοποίηση"), ("Your Device", "Η συσκευή σας"), - ("empty_recent_tip", "Δεν υπάρχουν πρόσφατες συνεδρίες!\nΔοκιμάστε να ξεκινήσετε μια νέα."), - ("empty_favorite_tip", "Δεν υπάρχουν ακόμη αγαπημένες συνδέσεις;\nΑφού πραγματοποιήσετε σύνδεση με κάποιο απομακρυσμένο σταθμό, μπορείτε να τον προσθέσετε στα αγαπημένα σας!"), - ("empty_lan_tip", "Δεν έχουμε ανακαλυφθεί ακόμη απομακρυσμένοι σταθμοί."), - ("empty_address_book_tip", "Φαίνεται ότι αυτή τη στιγμή δεν υπάρχουν αγαπημένες συνδέσεις στο βιβλίο διευθύνσεών σας."), + ("empty_recent_tip", "Ωχ, δεν υπάρχουν πρόσφατες συνεδρίες!\nΏρα να προγραμματίσετε μια νέα."), + ("empty_favorite_tip", "Δεν έχετε ακόμα αγαπημένους απομακρυσμένους σταθμούς;\nΑς βρούμε κάποιον για να συνδεθούμε και ας τον προσθέσουμε στα αγαπημένα σας!"), + ("empty_lan_tip", "Ωχ όχι, φαίνεται ότι δεν έχουμε ανακαλύψει ακόμη κανέναν απομακρυσμένο σταθμό."), + ("empty_address_book_tip", "Ω, Αγαπητέ/ή μου, φαίνεται ότι αυτήν τη στιγμή δεν υπάρχουν απομακρυσμένοι σταθμοί στο βιβλίο διευθύνσεών σας."), ("Empty Username", "Κενό όνομα χρήστη"), ("Empty Password", "Κενός κωδικός πρόσβασης"), ("Me", "Εγώ"), - ("identical_file_tip", "Το αρχείο είναι πανομοιότυπο με αυτό του άλλου υπολογιστή."), + ("identical_file_tip", "Αυτό το αρχείο είναι πανομοιότυπο με αυτό του απομακρυσμένου σταθμού."), ("show_monitors_tip", "Εμφάνιση οθονών στη γραμμή εργαλείων"), ("View Mode", "Λειτουργία προβολής"), - ("login_linux_tip", "Απαιτείται είσοδος σε απομακρυσμένο λογαριασμό Linux για την ενεργοποίηση του περιβάλλον εργασίας Χ."), + ("login_linux_tip", "Πρέπει να συνδεθείτε σε έναν απομακρυσμένο λογαριασμό Linux για να ενεργοποιήσετε μια συνεδρία επιφάνειας εργασίας X"), ("verify_rustdesk_password_tip", "Επιβεβαιώστε τον κωδικό του RustDesk"), ("remember_account_tip", "Απομνημόνευση αυτού του λογαριασμού"), - ("os_account_desk_tip", "Αυτός ο λογαριασμός θα χρησιμοποιηθεί για την είσοδο και διαχείριση του απομακρυσμένου λειτουργικού συστήματος"), + ("os_account_desk_tip", "Αυτός ο λογαριασμός χρησιμοποιείται για σύνδεση στο απομακρυσμένο λειτουργικό σύστημα και ενεργοποίηση της συνεδρίας επιφάνειας εργασίας σε headless"), ("OS Account", "Λογαριασμός λειτουργικού συστήματος"), ("another_user_login_title_tip", "Υπάρχει ήδη άλλος συνδεδεμένος χρήστης"), ("another_user_login_text_tip", "Αποσύνδεση"), ("xorg_not_found_title_tip", "Δεν βρέθηκε το Xorg"), ("xorg_not_found_text_tip", "Παρακαλώ εγκαταστήστε το Xorg"), - ("no_desktop_title_tip", "Δεν υπάρχει διαθέσιμη επιφάνεια εργασίας"), + ("no_desktop_title_tip", "Δεν υπάρχει διαθέσιμο περιβάλλον επιφάνειας εργασίας"), ("no_desktop_text_tip", "Παρακαλώ εγκαταστήστε το περιβάλλον GNOME"), - ("No need to elevate", "Δεν χρειάζονται αυξημένα δικαιώματα"), + ("No need to elevate", "Δεν χρειάζεται ανύψωση"), ("System Sound", "Ήχος συστήματος"), ("Default", "Προκαθορισμένο"), - ("New RDP", "Νέα απομακρυσμένη σύνδεση"), - ("Fingerprint", ""), - ("Copy Fingerprint", ""), - ("no fingerprints", ""), - ("Select a peer", "Επιλέξτε σταθμό"), + ("New RDP", "Νέα RDP"), + ("Fingerprint", "Δακτυλικό αποτύπωμα"), + ("Copy Fingerprint", "Αντιγραφή δακτυλικού αποτυπώματος"), + ("no fingerprints", "χωρίς δακτυλικά αποτυπώματα"), + ("Select a peer", "Επιλέξτε έναν σταθμό"), ("Select peers", "Επιλέξτε σταθμούς"), ("Plugins", "Επεκτάσεις"), ("Uninstall", "Κατάργηση εγκατάστασης"), @@ -493,10 +493,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("resolution_original_tip", "Αρχική ανάλυση"), ("resolution_fit_local_tip", "Προσαρμογή στην τοπική ανάλυση"), ("resolution_custom_tip", "Προσαρμοσμένη ανάλυση"), - ("Collapse toolbar", "Εμφάνιση γραμμής εργαλείων"), - ("Accept and Elevate", "Αποδοχή με αυξημένα δικαιώματα"), - ("accept_and_elevate_btn_tooltip", "Αποδοχή της σύνδεσης με αυξημένα δικαιώματα χρήστη"), - ("clipboard_wait_response_timeout_tip", "Έληξε ο χρόνος αναμονής για την ανταπόκριση της αντιγραφής"), + ("Collapse toolbar", "Σύμπτυξη γραμμής εργαλείων"), + ("Accept and Elevate", "Αποδοχή και ανύψωση"), + ("accept_and_elevate_btn_tooltip", "Αποδεχτείτε τη σύνδεση και ανυψώστε τα δικαιώματα UAC."), + ("clipboard_wait_response_timeout_tip", "Λήξη χρονικού ορίου αναμονής για απάντηση αντιγραφής."), ("Incoming connection", "Εισερχόμενη σύνδεση"), ("Outgoing connection", "Εξερχόμενη σύνδεση"), ("Exit", "Έξοδος"), @@ -505,7 +505,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Service", "Υπηρεσία"), ("Start", "Έναρξη"), ("Stop", "Διακοπή"), - ("exceed_max_devices", "Υπέρβαση μέγιστου ορίου αποθηκευμένων συνδέσεων"), + ("exceed_max_devices", "Έχετε φτάσει τον μέγιστο αριθμό διαχειριζόμενων συσκευών."), ("Sync with recent sessions", "Συγχρονισμός των πρόσφατων συνεδριών"), ("Sort tags", "Ταξινόμηση ετικετών"), ("Open connection in new tab", "Άνοιγμα σύνδεσης σε νέα καρτέλα"), @@ -514,14 +514,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Already exists", "Υπάρχει ήδη"), ("Change Password", "Αλλαγή κωδικού"), ("Refresh Password", "Ανανέωση κωδικού"), - ("ID", ""), + ("ID", "ID"), ("Grid View", "Προβολή σε πλακίδια"), ("List View", "Προβολή σε λίστα"), ("Select", "Επιλογή"), ("Toggle Tags", "Εναλλαγή ετικετών"), - ("pull_ab_failed_tip", "Αποτυχία ανανέωσης βιβλίου διευθύνσεων"), - ("push_ab_failed_tip", "Αποτυχία συγχρονισμού βιβλίου διευθύνσεων"), - ("synced_peer_readded_tip", "Οι συσκευές των τρεχουσών συνεδριών θα συγχρονιστούν με το βιβλίο διευθύνσεων"), + ("pull_ab_failed_tip", "Η ανανέωση του βιβλίου διευθύνσεων απέτυχε"), + ("push_ab_failed_tip", "Αποτυχία συγχρονισμού του βιβλίου διευθύνσεων με τον διακομιστή"), + ("synced_peer_readded_tip", "Οι συσκευές που υπήρχαν στις πρόσφατες συνεδρίες θα συγχρονιστούν ξανά με το βιβλίο διευθύνσεων."), ("Change Color", "Αλλαγή χρώματος"), ("Primary Color", "Κυρίως χρώμα"), ("HSV Color", "Χρώμα HSV"), @@ -536,31 +536,31 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("I Agree", "Συμφωνώ"), ("Decline", "Διαφωνώ"), ("Timeout in minutes", "Τέλος χρόνου σε λεπτά"), - ("auto_disconnect_option_tip", "Αυτόματη αποσύνδεση απομακρυσμένης συνεδρίας έπειτα από την πάροδο του χρονικού ορίου αδράνειας "), + ("auto_disconnect_option_tip", "Αυτόματο κλείσιμο εισερχόμενων συνεδριών σε περίπτωση αδράνειας χρήστη"), ("Connection failed due to inactivity", "Η σύνδεση τερματίστηκε έπειτα από την πάροδο του χρόνου αδράνειας"), - ("Check for software update on startup", "Έλεγχος για ενημερώσεις κατα την εκκίνηση"), - ("upgrade_rustdesk_server_pro_to_{}_tip", "Παρακαλώ ενημερώστε τον RustDesk Server Pro στην έκδοση {} ή νεότερη!"), + ("Check for software update on startup", "Έλεγχος για ενημερώσεις κατά την εκκίνηση"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Παρακαλώ ενημερώστε το RustDesk Server Pro στην έκδοση {} ή νεότερη!"), ("pull_group_failed_tip", "Αποτυχία ανανέωσης της ομάδας"), - ("Filter by intersection", ""), + ("Filter by intersection", "Φιλτράρισμα κατά διασταύρωση"), ("Remove wallpaper during incoming sessions", "Αφαίρεση εικόνας φόντου στις εισερχόμενες συνδέσεις"), ("Test", "Δοκιμή"), - ("display_is_plugged_out_msg", "Η οθόνη έχει αποσυνδεθεί, επιστρέψτε στην κύρια οθόνη προβολής"), + ("display_is_plugged_out_msg", "Η οθόνη είναι αποσυνδεδεμένη από την πρίζα, μεταβείτε στην πρώτη οθόνη."), ("No displays", "Δεν υπάρχουν οθόνες"), ("Open in new window", "Άνοιγμα σε νέο παράθυρο"), ("Show displays as individual windows", "Εμφάνιση οθονών σε ξεχωριστά παράθυρα"), ("Use all my displays for the remote session", "Χρήση όλων των οθονών της απομακρυσμένης σύνδεσης"), - ("selinux_tip", "Έχετε ενεργοποιημένο το SELinux, το οποίο πιθανόν εμποδίζει την ορθή λειτουργία του RustDesk."), + ("selinux_tip", "Το SELinux είναι ενεργοποιημένο στη συσκευή σας, κάτι που ενδέχεται να εμποδίσει την σωστή λειτουργία του RustDesk ως ελεγχόμενης πλευράς."), ("Change view", "Αλλαγή απεικόνισης"), ("Big tiles", "Μεγάλα εικονίδια"), ("Small tiles", "Μικρά εικονίδια"), ("List", "Λίστα"), ("Virtual display", "Εινονική οθόνη"), ("Plug out all", "Αποσύνδεση όλων"), - ("True color (4:4:4)", ""), + ("True color (4:4:4)", "Αληθινό χρώμα (4:4:4)"), ("Enable blocking user input", "Ενεργοποίηση αποκλεισμού χειρισμού από τον χρήστη"), - ("id_input_tip", "Μπορείτε να εισάγετε ενα ID, μια διεύθυνση IP, ή ένα όνομα τομέα με την αντίστοιχη πόρτα (:).\nΑν θέλετε να συνδεθείτε σε μια συσκευή σε άλλο διακομιστή, παρακαλώ να προσθέσετε και την διεύθυνση του διακομιστή (@?key=), για παράδειγμα,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nΑν θέλετε να συνδεθείτε σε κάποιο δημόσιο διακομιστή, προσθέστε το όνομά του \"@public\", η παράμετρος key δεν απαιτείται για τους δημόσιους διακομιστές."), - ("privacy_mode_impl_mag_tip", "Προφύλαξη Οθόνης"), - ("privacy_mode_impl_virtual_display_tip", "Εικονική Οθόνη"), + ("id_input_tip", "Μπορείτε να εισάγετε ένα ID, μια διεύθυνση IP, ή ένα όνομα τομέα με την αντίστοιχη πόρτα (:).\nΑν θέλετε να συνδεθείτε σε μια συσκευή σε άλλο διακομιστή, παρακαλώ να προσθέσετε και την διεύθυνση του διακομιστή (@?key=), για παράδειγμα,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nΑν θέλετε να συνδεθείτε σε κάποιο δημόσιο διακομιστή, προσθέστε το όνομά του \"@public\", η παράμετρος key δεν απαιτείται για τους δημόσιους διακομιστές."), + ("privacy_mode_impl_mag_tip", "Λειτουργία 1"), + ("privacy_mode_impl_virtual_display_tip", "Λειτουργία 2"), ("Enter privacy mode", "Ενεργοποίηση λειτουργίας απορρήτου"), ("Exit privacy mode", "Διακοπή λειτουργίας απορρήτου"), ("idd_not_support_under_win10_2004_tip", "Το πρόγραμμα οδήγησης έμμεσης οθόνης δεν υποστηρίζεται. Απαιτείτε λειτουργικό σύστημα Windows 10 έκδοση 2004 ή νεότερο."), @@ -570,26 +570,26 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("swap-left-right-mouse", "Εναλλαγή αριστερό-δεξί κουμπί του ποντικιού"), ("2FA code", "κωδικός 2FA"), ("More", "Περισσότερα"), - ("enable-2fa-title", "Ενεργοποίηση Πιστοποίησης Δύο Παραγόντων"), - ("enable-2fa-desc", "Ρυθμίστε τον έλεγχο ταυτότητας τώρα. Μπορείτε να χρησιμοποιήσετε μια εφαρμογή ελέγχου ταυτότητας όπως Authy, Microsoft ή Google Authenticator στο τηλέφωνο ή στην επιφάνεια εργασίας σας.Σαρώστε τον κωδικό QR με την εφαρμογή σας και εισαγάγετε τον κωδικό που εμφανίζει η εφαρμογή σας για να ενεργοποιήσετε τον έλεγχο ταυτότητας δύο παραγόντων."), - ("wrong-2fa-code", "Δεν είναι δυνατή η επαλήθευση του κωδικού. Ελέγξτε ότι ο κωδικός και οι ρυθμίσεις τοπικής ώρας είναι σωστές"), + ("enable-2fa-title", "Ενεργοποίηση πιστοποίησης δύο παραγόντων"), + ("enable-2fa-desc", "Παρακαλούμε να ρυθμίστε τώρα τον έλεγχο ταυτότητας. Μπορείτε να χρησιμοποιήσετε μια εφαρμογή ελέγχου ταυτότητας όπως το Authy, το Microsoft ή το Google Authenticator στο τηλέφωνο ή τον υπολογιστή σας.\n\nΣαρώστε τον κωδικό QR με την εφαρμογή σας και εισαγάγετε τον κωδικό που εμφανίζει η εφαρμογή σας για να ενεργοποιήσετε τον έλεγχο ταυτότητας δύο παραγόντων."), + ("wrong-2fa-code", "Δεν είναι δυνατή η επαλήθευση του κωδικού. Ελέγξτε ότι οι ρυθμίσεις κωδικού και τοπικής ώρας είναι σωστές."), ("enter-2fa-title", "Έλεγχος ταυτότητας δύο παραγόντων"), - ("Email verification code must be 6 characters.", "Ο κωδικός επαλήθευσης email πρέπει να είναι εως 6 χαρακτήρες"), + ("Email verification code must be 6 characters.", "Ο κωδικός επαλήθευσης email πρέπει να είναι έως 6 χαρακτήρες"), ("2FA code must be 6 digits.", "Ο κωδικός 2FA πρέπει να είναι 6ψήφιος."), - ("Multiple Windows sessions found", ""), + ("Multiple Windows sessions found", "Βρέθηκαν πολλές συνεδρίες των Windows"), ("Please select the session you want to connect to", "Επιλέξτε τη συνεδρία στην οποία θέλετε να συνδεθείτε"), - ("powered_by_me", "Με την υποστήριξη της RustDesk"), - ("outgoing_only_desk_tip", ""), - ("preset_password_warning", "προειδοποίηση προκαθορισμένου κωδικού πρόσβασης"), + ("powered_by_me", "Με την υποστήριξη του RustDesk"), + ("outgoing_only_desk_tip", "Αυτή είναι μια προσαρμοσμένη έκδοση.\nΜπορείτε να συνδεθείτε με άλλες συσκευές, αλλά άλλες συσκευές δεν μπορούν να συνδεθούν με τη δική σας συσκευή."), + ("preset_password_warning", "Αυτή η προσαρμοσμένη έκδοση συνοδεύεται από έναν προκαθορισμένο κωδικό πρόσβασης. Όποιος γνωρίζει αυτόν τον κωδικό πρόσβασης θα μπορούσε να αποκτήσει τον πλήρη έλεγχο της συσκευής σας. Εάν δεν το περιμένατε αυτό, απεγκαταστήστε αμέσως το λογισμικό."), ("Security Alert", "Ειδοποίηση ασφαλείας"), ("My address book", "Το βιβλίο διευθύνσεών μου"), ("Personal", "Προσωπικό"), ("Owner", "Ιδιοκτήτης"), - ("Set shared password", "Ορίστε κοινόχρηστο κωδικό πρόσβασης"), + ("Set shared password", "Ορίστε έναν κοινόχρηστο κωδικό πρόσβασης"), ("Exist in", "Υπάρχει στο"), ("Read-only", "Μόνο για ανάγνωση"), ("Read/Write", "Ανάγνωση/Εγγραφή"), - ("Full Control", "Πλήρης Έλεγχος"), + ("Full Control", "Πλήρης έλεγχος"), ("share_warning_tip", "Τα παραπάνω πεδία είναι κοινόχρηστα και ορατά σε άλλους."), ("Everyone", "Όλοι"), ("ab_web_console_tip", "Περισσότερα στην κονσόλα web"), @@ -597,18 +597,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("no_need_privacy_mode_no_physical_displays_tip", "Δεν υπάρχουν φυσικές οθόνες, δεν χρειάζεται να χρησιμοποιήσετε τη λειτουργία απορρήτου."), ("Follow remote cursor", "Παρακολούθηση απομακρυσμένου κέρσορα"), ("Follow remote window focus", "Παρακολούθηση απομακρυσμένου ενεργού παραθύρου"), - ("default_proxy_tip", "Προκαθορισμένο πρωτόκολλο Socks5 στην πόρτα 1080"), + ("default_proxy_tip", "Το προεπιλεγμένο πρωτόκολλο και η θύρα είναι Socks5 και 1080"), ("no_audio_input_device_tip", "Δεν βρέθηκε συσκευή εισόδου ήχου."), ("Incoming", "Εισερχόμενη"), ("Outgoing", "Εξερχόμενη"), - ("Clear Wayland screen selection", ""), - ("clear_Wayland_screen_selection_tip", ""), - ("confirm_clear_Wayland_screen_selection_tip", ""), - ("android_new_voice_call_tip", ""), - ("texture_render_tip", ""), - ("Use texture rendering", ""), - ("Floating window", ""), - ("floating_window_tip", ""), + ("Clear Wayland screen selection", "Εκκαθάριση επιλογής οθόνης Wayland"), + ("clear_Wayland_screen_selection_tip", "Αφού διαγράψετε την επιλογή οθόνης, μπορείτε να επιλέξετε ξανά την οθόνη για κοινή χρήση."), + ("confirm_clear_Wayland_screen_selection_tip", "Είστε βέβαιοι ότι θέλετε να διαγράψετε την επιλογή οθόνης Wayland;"), + ("android_new_voice_call_tip", "Ελήφθη ένα νέο αίτημα φωνητικής κλήσης. Εάν το αποδεχτείτε, ο ήχος θα μεταβεί σε φωνητική επικοινωνία."), + ("texture_render_tip", "Χρησιμοποιήστε την απόδοση υφής για να κάνετε τις εικόνες πιο ομαλές. Μπορείτε να δοκιμάσετε να απενεργοποιήσετε αυτήν την επιλογή εάν αντιμετωπίσετε προβλήματα απόδοσης."), + ("Use texture rendering", "Χρήση απόδοσης υφής"), + ("Floating window", "Πλωτό παράθυρο"), + ("floating_window_tip", "Βοηθά στη διατήρηση της υπηρεσίας παρασκηνίου RustDesk"), ("Keep screen on", "Διατήρηση οθόνης Ανοιχτή"), ("Never", "Ποτέ"), ("During controlled", "Κατα την διάρκεια απομακρυσμένου ελέγχου"), @@ -618,8 +618,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Apps", "Εφαρμογές"), ("Volume up", "Αύξηση έντασης"), ("Volume down", "Μείωση έντασης"), - ("Power", ""), - ("Telegram bot", ""), + ("Power", "Ενέργεια"), + ("Telegram bot", "Telegram bot"), ("enable-bot-tip", "Εάν ενεργοποιήσετε αυτήν τη δυνατότητα, μπορείτε να λάβετε τον κωδικό 2FA από το bot σας. Μπορεί επίσης να λειτουργήσει ως ειδοποίηση σύνδεσης."), ("enable-bot-desc", "1, Ανοίξτε μια συνομιλία με τον @BotFather., Στείλτε την εντολή \"/newbot\". Θα λάβετε ένα διακριτικό αφού ολοκληρώσετε αυτό το βήμα.3, Ξεκινήστε μια συνομιλία με το bot που μόλις δημιουργήσατε. Στείλτε ένα μήνυμα που αρχίζει με κάθετο (\"/\") όπως \"/hello\" για να το ενεργοποιήσετε."), ("cancel-2fa-confirm-tip", "Είστε βέβαιοι ότι θέλετε να ακυρώσετε το 2FA;"), @@ -639,11 +639,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", "Γονικός φάκελος"), ("Resume", "Συνέχεια"), ("Invalid file name", "Μη έγκυρο όνομα αρχείου"), - ("one-way-file-transfer-tip", ""), + ("one-way-file-transfer-tip", "Η μονόδρομη μεταφορά αρχείων είναι ενεργοποιημένη στην ελεγχόμενη πλευρά."), ("Authentication Required", "Απαιτείται έλεγχος ταυτότητας"), ("Authenticate", "Πιστοποίηση"), - ("web_id_input_tip", ""), - ("Download", ""), + ("web_id_input_tip", "Μπορείτε να εισαγάγετε ένα ID στον ίδιο διακομιστή, η άμεση πρόσβαση IP δεν υποστηρίζεται στον web client.\nΕάν θέλετε να αποκτήσετε πρόσβαση σε μια συσκευή σε άλλον διακομιστή, παρακαλούμε να προσθέστε τη διεύθυνση διακομιστή (@?key=), για παράδειγμα,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nΕάν θέλετε να αποκτήσετε πρόσβαση σε μια συσκευή σε δημόσιο διακομιστή, παρακαλούμε να εισαγάγετε \"@public\". Το κλειδί δεν είναι απαραίτητο για δημόσιο διακομιστή."), + ("Download", "Λήψη"), ("Upload folder", "Μεταφόρτωση φακέλου"), ("Upload files", "Μεταφόρτωση αρχείων"), ("Clipboard is synchronized", "Το πρόχειρο έχει συγχρονιστεί"), @@ -652,93 +652,93 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("new-version-of-{}-tip", "Υπάρχει διαθέσιμη νέα έκδοση του {}"), ("Accessible devices", "Προσβάσιμες συσκευές"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Αναβαθμίστε τον πελάτη RustDesk στην έκδοση {} ή νεότερη στην απομακρυσμένη πλευρά!"), - ("d3d_render_tip", ""), - ("Use D3D rendering", ""), - ("Printer", ""), - ("printer-os-requirement-tip", ""), - ("printer-requires-installed-{}-client-tip", ""), - ("printer-{}-not-installed-tip", ""), - ("printer-{}-ready-tip", ""), - ("Install {} Printer", ""), - ("Outgoing Print Jobs", ""), - ("Incoming Print Jobs", ""), - ("Incoming Print Job", ""), - ("use-the-default-printer-tip", ""), - ("use-the-selected-printer-tip", ""), - ("auto-print-tip", ""), - ("print-incoming-job-confirm-tip", ""), - ("remote-printing-disallowed-tile-tip", ""), - ("remote-printing-disallowed-text-tip", ""), - ("save-settings-tip", ""), - ("dont-show-again-tip", ""), - ("Take screenshot", ""), - ("Taking screenshot", ""), - ("screenshot-merged-screen-not-supported-tip", ""), - ("screenshot-action-tip", ""), - ("Save as", ""), - ("Copy to clipboard", ""), - ("Enable remote printer", ""), - ("Downloading {}", ""), - ("{} Update", ""), - ("{}-to-update-tip", ""), - ("download-new-version-failed-tip", ""), - ("Auto update", ""), - ("update-failed-check-msi-tip", ""), - ("websocket_tip", ""), - ("Use WebSocket", ""), - ("Trackpad speed", ""), - ("Default trackpad speed", ""), - ("Numeric one-time password", ""), - ("Enable IPv6 P2P connection", ""), - ("Enable UDP hole punching", ""), + ("d3d_render_tip", "Όταν είναι ενεργοποιημένη η απόδοση D3D, η οθόνη του τηλεχειριστηρίου ενδέχεται να είναι μαύρη σε ορισμένα μηχανήματα."), + ("Use D3D rendering", "Χρήση απόδοσης D3D"), + ("Printer", "Εκτυπωτής"), + ("printer-os-requirement-tip", "Η λειτουργία εξερχόμενης εκτύπωσης του εκτυπωτή απαιτεί Windows 10 ή νεότερη έκδοση."), + ("printer-requires-installed-{}-client-tip", "Για να χρησιμοποιήσετε την απομακρυσμένη εκτύπωση, πρέπει να εγκατασταθεί το {} σε αυτήν τη συσκευή."), + ("printer-{}-not-installed-tip", "Ο εκτυπωτής {} δεν είναι εγκατεστημένος."), + ("printer-{}-ready-tip", "Ο εκτυπωτής {} είναι εγκατεστημένος και έτοιμος για χρήση."), + ("Install {} Printer", "Εγκατάσταση εκτυπωτή {}"), + ("Outgoing Print Jobs", "Εξερχόμενες εργασίες εκτύπωσης"), + ("Incoming Print Jobs", "Εισερχόμενες εργασίες εκτύπωσης"), + ("Incoming Print Job", "Εισερχόμενη εργασία εκτύπωσης"), + ("use-the-default-printer-tip", "Χρήση του προεπιλεγμένου εκτυπωτή"), + ("use-the-selected-printer-tip", "Χρήση του επιλεγμένου εκτυπωτή"), + ("auto-print-tip", "Εκτυπώστε αυτόματα χρησιμοποιώντας τον επιλεγμένο εκτυπωτή."), + ("print-incoming-job-confirm-tip", "Λάβατε μια εργασία εκτύπωσης από απόσταση. Θέλετε να την εκτελέσετε από την πλευρά σας;"), + ("remote-printing-disallowed-tile-tip", "Η απομακρυσμένη εκτύπωση δεν επιτρέπεται"), + ("remote-printing-disallowed-text-tip", "Οι ρυθμίσεις δικαιωμάτων της ελεγχόμενης πλευράς απαγορεύουν την Απομακρυσμένη Εκτύπωση."), + ("save-settings-tip", "Αποθήκευση ρυθμίσεων"), + ("dont-show-again-tip", "Να μην εμφανιστεί ξανά αυτό"), + ("Take screenshot", "Λήψη στιγμιότυπου οθόνης"), + ("Taking screenshot", "Γίνεται λήψη στιγμιότυπου οθόνης"), + ("screenshot-merged-screen-not-supported-tip", "Η συγχώνευση στιγμιότυπων οθόνης από πολλές οθόνες δεν υποστηρίζεται προς το παρόν. Αλλάξτε σε μία μόνο οθόνη και δοκιμάστε ξανά."), + ("screenshot-action-tip", "Επιλέξτε πώς θα συνεχίσετε με το στιγμιότυπο οθόνης."), + ("Save as", "Αποθήκευση ως"), + ("Copy to clipboard", "Αντιγραφή στο πρόχειρο"), + ("Enable remote printer", "Ενεργοποίηση απομακρυσμένου εκτυπωτή"), + ("Downloading {}", "Γίνεται Λήψη {}"), + ("{} Update", "{} Ενημέρωση"), + ("{}-to-update-tip", "Το {} θα κλείσει τώρα και θα εγκαταστήσει τη νέα έκδοση."), + ("download-new-version-failed-tip", "Η λήψη απέτυχε. Μπορείτε να δοκιμάσετε ξανά ή να κάνετε κλικ στο κουμπί \"Λήψη\" για να κάνετε λήψη από τη σελίδα έκδοσης και να κάνετε αναβάθμιση χειροκίνητα."), + ("Auto update", "Αυτόματη ενημέρωση"), + ("update-failed-check-msi-tip", "Η μέθοδος εγκατάστασης απέτυχε. Κάντε κλικ στο κουμπί \"Λήψη\" για λήψη από τη σελίδα έκδοσης και κάντε χειροκίνητα την αναβάθμιση."), + ("websocket_tip", "Όταν χρησιμοποιείτε το WebSocket, υποστηρίζονται μόνο συνδέσεις αναμετάδοσης."), + ("Use WebSocket", "Χρήση WebSocket"), + ("Trackpad speed", "Ταχύτητα trackpad"), + ("Default trackpad speed", "Προεπιλεγμένη ταχύτητα trackpad"), + ("Numeric one-time password", "Αριθμητικός κωδικός πρόσβασης μίας χρήσης"), + ("Enable IPv6 P2P connection", "Ενεργοποίηση σύνδεσης IPv6 P2P"), + ("Enable UDP hole punching", "Ενεργοποίηση διάτρησης οπών UDP"), ("View camera", "Προβολή κάμερας"), - ("Enable camera", ""), - ("No cameras", ""), - ("view_camera_unsupported_tip", ""), - ("Terminal", ""), - ("Enable terminal", ""), - ("New tab", ""), - ("Keep terminal sessions on disconnect", ""), - ("Terminal (Run as administrator)", ""), - ("terminal-admin-login-tip", ""), - ("Failed to get user token.", ""), - ("Incorrect username or password.", ""), - ("The user is not an administrator.", ""), - ("Failed to check if the user is an administrator.", ""), - ("Supported only in the installed version.", ""), - ("elevation_username_tip", ""), - ("Preparing for installation ...", ""), - ("Show my cursor", ""), - ("Scale custom", ""), - ("Custom scale slider", ""), - ("Decrease", ""), - ("Increase", ""), - ("Show virtual mouse", ""), - ("Virtual mouse size", ""), - ("Small", ""), - ("Large", ""), - ("Show virtual joystick", ""), - ("Edit note", ""), - ("Alias", ""), - ("ScrollEdge", ""), - ("Allow insecure TLS fallback", ""), - ("allow-insecure-tls-fallback-tip", ""), - ("Disable UDP", ""), - ("disable-udp-tip", ""), - ("server-oss-not-support-tip", ""), - ("input note here", ""), - ("note-at-conn-end-tip", ""), - ("Show terminal extra keys", ""), - ("Relative mouse mode", ""), - ("rel-mouse-not-supported-peer-tip", ""), - ("rel-mouse-not-ready-tip", ""), - ("rel-mouse-lock-failed-tip", ""), - ("rel-mouse-exit-{}-tip", ""), - ("rel-mouse-permission-lost-tip", ""), - ("Changelog", ""), - ("keep-awake-during-outgoing-sessions-label", ""), - ("keep-awake-during-incoming-sessions-label", ""), + ("Enable camera", "Ενεργοποίηση κάμερας"), + ("No cameras", "Δεν υπάρχουν κάμερες"), + ("view_camera_unsupported_tip", "Η τηλεχειριστήριο δεν υποστηρίζει την προβολή της κάμερας."), + ("Terminal", "Τερματικό"), + ("Enable terminal", "Ενεργοποίηση τερματικού"), + ("New tab", "Νέα καρτέλα"), + ("Keep terminal sessions on disconnect", "Διατήρηση περιόδων λειτουργίας τερματικού κατά την αποσύνδεση"), + ("Terminal (Run as administrator)", "Τερματικό (Εκτέλεση ως διαχειριστής)"), + ("terminal-admin-login-tip", "Παρακαλώ εισάγετε το όνομα χρήστη και τον κωδικό πρόσβασης διαχειριστή της ελεγχόμενης πλευράς."), + ("Failed to get user token.", "Αποτυχία λήψης διακριτικού χρήστη."), + ("Incorrect username or password.", "Λανθασμένο όνομα χρήστη ή κωδικός πρόσβασης."), + ("The user is not an administrator.", "Ο χρήστης δεν είναι διαχειριστής."), + ("Failed to check if the user is an administrator.", "Αποτυχία ελέγχου εάν ο χρήστης είναι διαχειριστής."), + ("Supported only in the installed version.", "Υποστηρίζεται μόνο στην εγκατεστημένη έκδοση."), + ("elevation_username_tip", "Εισαγάγετε όνομα χρήστη ή τομέα\\όνομα χρήστη"), + ("Preparing for installation ...", "Προετοιμασία για εγκατάσταση..."), + ("Show my cursor", "Εμφάνιση του κέρσορα μου"), + ("Scale custom", "Προσαρμοσμένη κλίμακα"), + ("Custom scale slider", "Ρυθμιστικό προσαρμοσμένης κλίμακας"), + ("Decrease", "Μείωση"), + ("Increase", "Αύξηση"), + ("Show virtual mouse", "Εμφάνιση εικονικού ποντικιού"), + ("Virtual mouse size", "Μέγεθος εικονικού ποντικιού"), + ("Small", "Μικρό"), + ("Large", "Μεγάλο"), + ("Show virtual joystick", "Εμφάνιση εικονικού joystick"), + ("Edit note", "Επεξεργασία σημείωσης"), + ("Alias", "Ψευδώνυμο"), + ("ScrollEdge", "Άκρη κύλισης"), + ("Allow insecure TLS fallback", "Να επιτρέπεται η μη ασφαλής εφεδρική λειτουργία TLS"), + ("allow-insecure-tls-fallback-tip", "Από προεπιλογή, το RustDesk επαληθεύει το πιστοποιητικό διακομιστή για πρωτόκολλα που χρησιμοποιούν TLS.\nΜε ενεργοποιημένη αυτήν την επιλογή, το RustDesk θα παρακάμψει το βήμα επαλήθευσης και θα προχωρήσει σε περίπτωση αποτυχίας επαλήθευσης."), + ("Disable UDP", "Απενεργοποίηση UDP"), + ("disable-udp-tip", "Ελέγχει εάν θα χρησιμοποιείται μόνο TCP.\nΌταν είναι ενεργοποιημένη αυτή η επιλογή, το RustDesk δεν θα χρησιμοποιεί πλέον το UDP 21116, αλλά θα χρησιμοποιείται το TCP 21116."), + ("server-oss-not-support-tip", "ΣΗΜΕΙΩΣΗ: Το OSS του διακομιστή RustDesk δεν περιλαμβάνει αυτήν τη λειτουργία."), + ("input note here", "εισάγετε σημείωση εδώ"), + ("note-at-conn-end-tip", "Ζητήστε σημείωση στο τέλος της σύνδεσης"), + ("Show terminal extra keys", "Εμφάνιση επιπλέον κλειδιών τερματικού"), + ("Relative mouse mode", "Σχετική λειτουργία ποντικιού"), + ("rel-mouse-not-supported-peer-tip", "Η λειτουργία σχετικού ποντικιού δεν υποστηρίζεται από τον συνδεδεμένο ομότιμο υπολογιστή."), + ("rel-mouse-not-ready-tip", "Η λειτουργία σχετικού ποντικιού δεν είναι ακόμη έτοιμη. Δοκιμάστε ξανά."), + ("rel-mouse-lock-failed-tip", "Αποτυχία κλειδώματος δρομέα. Η λειτουργία σχετικού ποντικιού έχει απενεργοποιηθεί."), + ("rel-mouse-exit-{}-tip", "Πιέστε {} για έξοδο."), + ("rel-mouse-permission-lost-tip", "Η άδεια πληκτρολογίου ανακλήθηκε. Η λειτουργία σχετικού ποντικιού απενεργοποιήθηκε."), + ("Changelog", "Αρχείο αλλαγών"), + ("keep-awake-during-outgoing-sessions-label", "Διατήρηση ενεργής οθόνης κατά τη διάρκεια εξερχόμενων συνεδριών"), + ("keep-awake-during-incoming-sessions-label", "Διατήρηση ενεργής οθόνης κατά τη διάρκεια των εισερχόμενων συνεδριών"), ("Continue with {}", "Συνέχεια με {}"), - ("Display Name", ""), + ("Display Name", "Εμφανιζόμενο όνομα"), ].iter().cloned().collect(); } From 272a6604cd7ab2d68d8f823d4889d2d3cf5f61c0 Mon Sep 17 00:00:00 2001 From: John Fowler Date: Tue, 24 Feb 2026 09:29:54 +0100 Subject: [PATCH 141/277] Hungarian language file correction (#14382) * Update Hungarian translations in hu.rs Translation of new strings and some fixes. John Fowler. * Escape quotes in Hungarian language strings Replacing Hungarian quotation marks * Update Hungarian translations for various terms Upload a new translation (hu.rs) file. * Hungarian language file correction New character strings translation, error correction. --- src/lang/hu.rs | 66 ++++++++++++++++++++++++-------------------------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 174cdb28b..03b601116 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -7,7 +7,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Password", "Jelszó"), ("Ready", "Kész"), ("Established", "Létrejött"), - ("connecting_status", "Kapcsolódás folyamatban…"), + ("connecting_status", "Kapcsolódás folyamatban ..."), ("Enable service", "Szolgáltatás engedélyezése"), ("Start service", "Szolgáltatás indítása"), ("Service is running", "Szolgáltatás aktív"), @@ -28,7 +28,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable file transfer", "Fájlátvitel engedélyezése"), ("Enable TCP tunneling", "TCP-alagút engedélyezése"), ("IP Whitelisting", "IP engedélyezési lista"), - ("ID/Relay Server", "Azonosító-/Továbbító-kiszolgáló"), + ("ID/Relay Server", "ID/Továbbító-kiszolgáló"), ("Import server config", "Kiszolgáló-konfiguráció importálása"), ("Export Server Config", "Kiszolgáló-konfiguráció exportálása"), ("Import server configuration successfully", "Kiszolgáló-konfiguráció sikeresen importálva"), @@ -54,7 +54,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enhancements", "Fejlesztések"), ("Hardware Codec", "Hardveres kodek"), ("Adaptive bitrate", "Adaptív bitráta"), - ("ID Server", "Azonosító-kiszolgáló"), + ("ID Server", "ID-kiszolgáló"), ("Relay Server", "Továbbító-kiszolgáló"), ("API Server", "API-kiszolgáló"), ("invalid_http", "A címnek mindenképpen http(s)://-el kell kezdődnie."), @@ -76,12 +76,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Connection Error", "Kapcsolódási hiba"), ("Error", "Hiba"), ("Reset by the peer", "A kapcsolatot a másik fél lezárta."), - ("Connecting...", "Kapcsolódás…"), - ("Connection in progress. Please wait.", "A kapcsolódás folyamatban van. Kis türelmet…"), + ("Connecting...", "Kapcsolódás..."), + ("Connection in progress. Please wait.", "A kapcsolódás folyamatban van. Kis türelmet ..."), ("Please try 1 minute later", "Próbálja meg 1 perc múlva"), ("Login Error", "Bejelentkezési hiba"), ("Successful", "Sikeres"), - ("Connected, waiting for image...", "Kapcsolódva, várakozás a képadatokra…"), + ("Connected, waiting for image...", "Kapcsolódva, várakozás a képadatokra..."), ("Name", "Név"), ("Type", "Típus"), ("Modified", "Módosított"), @@ -127,7 +127,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Optimize reaction time", "Gyorsan reagáló"), ("Custom", "Egyéni"), ("Show remote cursor", "Távoli kurzor megjelenítése"), - ("Show quality monitor", "Kapcsolat minőségének megjelenítése"), + ("Show quality monitor", "Kijelző minőségének ellenőrzése"), ("Disable clipboard", "Közös vágólap kikapcsolása"), ("Lock after session end", "Távoli fiók zárolása a munkamenet végén"), ("Insert Ctrl + Alt + Del", "Illessze be a Ctrl + Alt + Del billentyűzetkombinációt"), @@ -150,8 +150,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Configure", "Beállítás"), ("config_acc", "A számítógép távoli vezérléséhez a RustDesknek hozzáférési jogokat kell adnia."), ("config_screen", "Ahhoz, hogy távolról hozzáférhessen a számítógépéhez, meg kell adnia a RustDesknek a \"Képernyőfelvétel\" jogosultságot."), - ("Installing ...", "Telepítés…"), - ("Install", "Telepítés"), + ("Installing ...", "Telepítés ..."), + ("Install", "Telepítse"), ("Installation", "Telepítés"), ("Installation Path", "Telepítési útvonal"), ("Create start menu shortcuts", "Start menü parancsikonok létrehozása"), @@ -159,10 +159,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("agreement_tip", "A telepítés folytatásával automatikusan elfogadásra kerül a licenc szerződés."), ("Accept and Install", "Elfogadás és telepítés"), ("End-user license agreement", "Végfelhasználói licenc szerződés"), - ("Generating ...", "Előállítás…"), + ("Generating ...", "Létrehozás ..."), ("Your installation is lower version.", "A telepített verzió alacsonyabb."), ("not_close_tcp_tip", "Ne zárja be ezt az ablakot, amíg TCP-alagutat használ"), - ("Listening ...", "Figyelés…"), + ("Listening ...", "Figyelés ..."), ("Remote Host", "Távoli kiszolgáló"), ("Remote Port", "Távoli port"), ("Action", "Indítás"), @@ -177,7 +177,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept", "Elfogadás"), ("Dismiss", "Elutasítás"), ("Disconnect", "Kapcsolat bontása"), - ("Enable file copy and paste", "Fájlmásolás és -beillesztés engedélyezése"), + ("Enable file copy and paste", "Fájlmásolás és beillesztés engedélyezése"), ("Connected", "Kapcsolódva"), ("Direct and encrypted connection", "Közvetlen, és titkosított kapcsolat"), ("Relayed and encrypted connection", "Továbbított, és titkosított kapcsolat"), @@ -185,7 +185,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relayed and unencrypted connection", "Továbbított, és nem titkosított kapcsolat"), ("Enter Remote ID", "Távoli számítógép azonosítója"), ("Enter your password", "Adja meg a jelszavát"), - ("Logging in...", "Belépés folyamatban…"), + ("Logging in...", "Belépés folyamatban..."), ("Enable RDP session sharing", "RDP-munkamenet-megosztás engedélyezése"), ("Auto Login", "Automatikus bejelentkezés"), ("Enable direct IP access", "Közvetlen IP-elérés engedélyezése"), @@ -219,7 +219,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("verification_tip", "A regisztrált e-mail-címre egy ellenőrző kód lesz elküldve. Adja meg az ellenőrző kódot az újbóli bejelentkezéshez."), ("Logout", "Kilépés"), ("Tags", "Címkék"), - ("Search ID", "Azonosító keresése…"), + ("Search ID", "Azonosító keresése..."), ("whitelist_sep", "A címeket vesszővel, pontosvesszővel, szóközzel vagy új sorral kell elválasztani"), ("Add ID", "Azonosító hozzáadása"), ("Add Tag", "Címke hozzáadása"), @@ -258,10 +258,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Three-Finger vertically", "Három ujj függőlegesen"), ("Mouse Wheel", "Egérgörgő"), ("Two-Finger Move", "Kétujjas mozgatás"), - ("Canvas Move", "Vászon mozgatása"), + ("Canvas Move", "Nézet módosítása"), ("Pinch to Zoom", "Kétujjas nagyítás"), - ("Canvas Zoom", "Vászon nagyítása"), - ("Reset canvas", "Vászon visszaállítása"), + ("Canvas Zoom", "Nézet nagyítása"), + ("Reset canvas", "Nézet visszaállítása"), ("No permission of file transfer", "Nincs engedély a fájlátvitelre"), ("Note", "Megjegyzés"), ("Connection", "Kapcsolat"), @@ -314,7 +314,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable remote restart", "Távoli újraindítás engedélyezése"), ("Restart remote device", "Távoli eszköz újraindítása"), ("Are you sure you want to restart", "Biztosan újra szeretné indítani?"), - ("Restarting remote device", "Távoli eszköz újraindítása…"), + ("Restarting remote device", "Távoli eszköz újraindítása..."), ("remote_restarting_tip", "A távoli eszköz újraindul, zárja be ezt az üzenetet, kapcsolódjon újra az állandó jelszavával"), ("Copied", "Másolva"), ("Exit Fullscreen", "Kilépés teljes képernyős módból"), @@ -369,12 +369,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN discovery", "Felfedezés tiltása"), ("Write a message", "Üzenet írása"), ("Prompt", "Kérés"), - ("Please wait for confirmation of UAC...", "Várjon az UAC megerősítésére…"), + ("Please wait for confirmation of UAC...", "Várjon az UAC megerősítésére..."), ("elevated_foreground_window_tip", "A távvezérelt számítógép jelenleg nyitott ablakához magasabb szintű jogok szükségesek. Ezért jelenleg nem lehetséges az egér és a billentyűzet használata. Kérje meg azt a felhasználót, akinek a számítógépét távolról vezérli, hogy minimalizálja az ablakot, vagy növelje a jogokat. A jövőbeni probléma elkerülése érdekében ajánlott a szoftvert a távvezérelt számítógépre telepíteni."), ("Disconnected", "Kapcsolat bontva"), ("Other", "Egyéb"), ("Confirm before closing multiple tabs", "Biztosan bezárja az összes lapot?"), - ("Keyboard Settings", "Billentyűzet-beállítások"), + ("Keyboard Settings", "Billentyűzetbeállítások"), ("Full Access", "Teljes hozzáférés"), ("Screen Share", "Képernyőmegosztás"), ("Wayland requires Ubuntu 21.04 or higher version.", "A Waylandhez Ubuntu 21.04 vagy újabb verzió szükséges."), @@ -389,7 +389,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via password", "Munkamenetek elfogadása jelszóval"), ("Accept sessions via click", "Munkamenetek elfogadása kattintással"), ("Accept sessions via both", "Munkamenetek fogadása mindkettőn keresztül"), - ("Please wait for the remote side to accept your session request...", "Várjon, amíg a távoli oldal elfogadja a munkamenet-kérelmét…"), + ("Please wait for the remote side to accept your session request...", "Várjon, amíg a távoli oldal elfogadja a munkamenet-kérelmét..."), ("One-time Password", "Egyszer használatos jelszó"), ("Use one-time password", "Használjon ideiglenes jelszót"), ("One-time password length", "Egyszer használatos jelszó hossza"), @@ -447,13 +447,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resolution", "Felbontás"), ("No transfers in progress", "Nincs folyamatban átvitel"), ("Set one-time password length", "Állítsa be az egyszeri jelszó hosszát"), - ("RDP Settings", "RDP-beállítások"), + ("RDP Settings", "RDP beállítások"), ("Sort by", "Rendezés"), ("New Connection", "Új kapcsolat"), ("Restore", "Visszaállítás"), ("Minimize", "Minimalizálás"), ("Maximize", "Maximalizálás"), - ("Your Device", "Saját eszköz"), + ("Your Device", "Az én eszközöm"), ("empty_recent_tip", "Nincsenek aktuális munkamenetek!\nIdeje ütemezni egy újat."), ("empty_favorite_tip", "Még nincs kedvenc távoli állomása?\nHagyja, hogy találjunk valakit, akivel kapcsolatba tud lépni, és adja hozzá a kedvencekhez!"), ("empty_lan_tip", "Úgy tűnik, még nem adott hozzá egyetlen távoli helyszínt sem."), @@ -468,7 +468,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("verify_rustdesk_password_tip", "RustDesk jelszó megerősítése"), ("remember_account_tip", "Emlékezzen erre a fiókra"), ("os_account_desk_tip", "Ezzel a fiókkal bejelentkezhet a távoli operációs rendszerbe, és aktiválhatja az asztali munkamenetet fej nélküli módban."), - ("OS Account", "OS-fiók"), + ("OS Account", "OS fiók"), ("another_user_login_title_tip", "Egy másik felhasználó már bejelentkezett."), ("another_user_login_text_tip", "Különálló"), ("xorg_not_found_title_tip", "Xorg nem található."), @@ -568,7 +568,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input_source_2_tip", "2. bemeneti forrás"), ("Swap control-command key", "Vezérlő- és parancsgombok cseréje"), ("swap-left-right-mouse", "Bal és jobb egérgomb felcserélése"), - ("2FA code", "2FA-kód"), + ("2FA code", "2FA kód"), ("More", "Továbbiak"), ("enable-2fa-title", "Kétfaktoros hitelesítés aktiválása"), ("enable-2fa-desc", "Állítsa be a hitelesítőt. Használhat egy hitelesítő alkalmazást, például az Aegis, Authy, a Microsoft- vagy a Google Authenticator alkalmazást a telefonján vagy az asztali számítógépén.\n\nOlvassa be a QR-kódot az alkalmazással, és adja meg az alkalmazás által megjelenített kódot a kétfaktoros hitelesítés aktiválásához."), @@ -647,13 +647,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "Mappa feltöltése"), ("Upload files", "Fájlok feltöltése"), ("Clipboard is synchronized", "A vágólap szinkronizálva van"), - ("Update client clipboard", "Kliens vágólapjának frissítése"), + ("Update client clipboard", "Az ügyfél vágólapjának frissítése"), ("Untagged", "Címkézetlen"), ("new-version-of-{}-tip", "A(z) {} új verziója"), ("Accessible devices", "Hozzáférhető eszközök"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Frissítse a RustDesk klienst {} vagy újabb verziójára a távoli oldalon!"), - ("d3d_render_tip", "D3D-leképezés"), - ("Use D3D rendering", "D3D-leképezés használata"), + ("d3d_render_tip", "D3D leképezés"), + ("Use D3D rendering", "D3D leképezés használata"), ("Printer", "Nyomtató"), ("printer-os-requirement-tip", "Nyomtató operációs rendszerének minimális rendszerkövetelménye"), ("printer-requires-installed-{}-client-tip", "A nyomtatóhoz szükséges a(z) {} kliens telepítése"), @@ -672,7 +672,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("save-settings-tip", "Beállítások mentése"), ("dont-show-again-tip", "Ne jelenítse meg újra"), ("Take screenshot", "Képernyőkép készítése"), - ("Taking screenshot", "Képernyőkép készítése…"), + ("Taking screenshot", "Képernyőkép készítése..."), ("screenshot-merged-screen-not-supported-tip", "Egyesített képernyőről nem támogatott a képernyőkép készítése"), ("screenshot-action-tip", "Képernyőkép-művelet"), ("Save as", "Mentés másként"), @@ -680,7 +680,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable remote printer", "Távoli nyomtatók engedélyezése"), ("Downloading {}", "{} letöltése"), ("{} Update", "{} frissítés"), - ("{}-to-update-tip", "A(z) {} bezárása és az új verzió telepítése."), + ("{}-to-update-tip", "{} bezárása és az új verzió telepítése."), ("download-new-version-failed-tip", "Ha a letöltés sikertelen, akkor vagy újrapróbálkozhat, vagy a \"Letöltés\" gombra kattintva letöltheti a kiadási oldalról, és manuálisan frissíthet."), ("Auto update", "Automatikus frissítés"), ("update-failed-check-msi-tip", "A telepítési módszer felismerése nem sikerült. Kattintson a \"Letöltés\" gombra, hogy letöltse a kiadási oldalról, és manuálisan frissítse."), @@ -707,7 +707,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", "Hiba merült fel annak ellenőrzése során, hogy a felhasználó rendszergazda-e."), ("Supported only in the installed version.", "Csak a telepített változatban támogatott."), ("elevation_username_tip", "Felhasználónév vagy tartománynév megadása"), - ("Preparing for installation ...", "Felkészülés a telepítésre…"), + ("Preparing for installation ...", "Felkészülés a telepítésre ..."), ("Show my cursor", "Kurzor megjelenítése"), ("Scale custom", "Egyéni méretarány"), ("Custom scale slider", "Egyéni méretarány-csúszka"), @@ -733,12 +733,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-not-supported-peer-tip", "A kapcsolódott partner nem támogatja a relatív egér módot."), ("rel-mouse-not-ready-tip", "A relatív egér mód még nem elérhető. Próbálja meg újra."), ("rel-mouse-lock-failed-tip", "Nem sikerült zárolni a kurzort. A relatív egér mód le lett tiltva."), - ("rel-mouse-exit-{}-tip", "A kilépéshez nyomja meg a(z) {} gombot."), + ("rel-mouse-exit-{}-tip", "A kilépéshez nyomja meg a következő gombot: {}"), ("rel-mouse-permission-lost-tip", "A billentyűzet-hozzáférés vissza lett vonva. A relatív egér mód le lett tilva."), ("Changelog", "Változáslista"), ("keep-awake-during-outgoing-sessions-label", "Képernyő aktív állapotban tartása a kimenő munkamenetek során"), ("keep-awake-during-incoming-sessions-label", "Képernyő aktív állapotban tartása a bejövő munkamenetek során"), - ("Continue with {}", "Folytatás a következővel: {}"), - ("Display Name", ""), ].iter().cloned().collect(); } From 91ac48912e386227a8a2474542a9b8041077326c Mon Sep 17 00:00:00 2001 From: Lynilia <89228568+Lynilia@users.noreply.github.com> Date: Tue, 24 Feb 2026 09:30:12 +0100 Subject: [PATCH 142/277] Update fr.rs (#14383) --- src/lang/fr.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 1d54448c9..fed35727e 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -313,7 +313,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set permanent password", "Définir le mot de passe permanent"), ("Enable remote restart", "Activer le redémarrage à distance"), ("Restart remote device", "Redémarrer l’appareil distant"), - ("Are you sure you want to restart", "Voulez-vous vraiment redémarrer l’appareil ?"), + ("Are you sure you want to restart", "Voulez-vous vraiment redémarrer"), ("Restarting remote device", "Redémarrage de l’appareil distant"), ("remote_restarting_tip", "L'appareil distant redémarre ; veuillez fermer cette boîte de dialogue et vous reconnecter en utilisant le mot de passe permanent dans quelques instants"), ("Copied", "Copié"), @@ -739,6 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", "Maintenir l’écran allumé lors des sessions sortantes"), ("keep-awake-during-incoming-sessions-label", "Maintenir l’écran allumé lors des sessions entrantes"), ("Continue with {}", "Continuer avec {}"), - ("Display Name", ""), + ("Display Name", "Nom d’affichage"), ].iter().cloned().collect(); } From 50c62d5eacc12929af0982585221378a6d638cb6 Mon Sep 17 00:00:00 2001 From: bilimiyorum <131397022+bilimiyorum@users.noreply.github.com> Date: Tue, 24 Feb 2026 11:30:32 +0300 Subject: [PATCH 143/277] Update tr.rs (#14376) New string entry --- src/lang/tr.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/tr.rs b/src/lang/tr.rs index ac8b3d368..e70d0a497 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -739,6 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", "Giden oturumlar süresince ekranı açık tutun"), ("keep-awake-during-incoming-sessions-label", "Gelen oturumlar süresince ekranı açık tutun"), ("Continue with {}", "{} ile devam et"), - ("Display Name", ""), + ("Display Name", "Görünen Ad"), ].iter().cloned().collect(); } From 00160339375eeb6d54fa847b2b937d4a2426817c Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 24 Feb 2026 21:12:06 +0800 Subject: [PATCH 144/277] feat(terminal): add reconnection buffer support for persistent sessions (#14377) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(terminal): add reconnection buffer support for persistent sessions Fix two related issues: 1. Reconnecting to persistent sessions shows blank screen - server now automatically sends historical buffer on reconnection via SessionState machine with pending_buffer, eliminating the need for client-initiated buffer requests. 2. Terminal output before view ready causes NaN errors - buffer output chunks on client side until terminal view has valid dimensions, then flush in order on first valid resize. Rust side: - Introduce SessionState enum (Closed/Active) replacing bool is_opened - Auto-attach pending buffer on reconnection in handle_open() - Always drain output channel in read_outputs() to prevent overflow - Increase channel buffer from 100 to 500 - Optimize get_recent() to collect whole chunks (avoids ANSI truncation) - Extract create_terminal_data_response() helper (DRY) - Add reconnected flag to TerminalOpened protobuf message Flutter side: - Buffer output chunks until terminal view has valid dimensions - Flush buffered output on first valid resize via _markViewReady() - Clear terminal on reconnection to avoid duplicate output from buffer replay - Fix max_bytes type (u32) to match protobuf definition - Pass reconnected field through FlutterHandler event Signed-off-by: fufesou * fix(terminal): add two-phase SIGWINCH for TUI app redraw and session remap on reconnection Fix TUI apps (top, htop) not redrawing after reconnection. A single resize-then-restore is too fast for ncurses to detect a size change, so split across two read_outputs() polling cycles (~30ms apart) to force a full redraw. Also fix reconnection failure when client terminal_id doesn't match any surviving server-side session ID by remapping the lowest surviving session to the requested ID. Rust side: - Add two-phase SIGWINCH state machine (SigwinchPhase: TempResize → Restore → Idle) with retry logic (max 3 attempts per phase) - Add do_sigwinch_resize() for cross-platform PTY resize (direct PTY and Windows helper mode) - Add session remap logic for non-contiguous terminal_id reconnection - Extract try_send_output() helper with rate-limited drop logging (DRY) - Add 3-byte limit to UTF-8 continuation byte skipping in get_recent() to prevent runaway on non-UTF-8 binary data - Remove reconnected flag from flutter.rs (unused on client side) Flutter side: - Add reconnection screen clear and deferred flush logic - Filter self from persistent_sessions restore list - Add comments for web-related changes Signed-off-by: fufesou --------- Signed-off-by: fufesou --- flutter/lib/common.dart | 5 + .../lib/desktop/pages/terminal_tab_page.dart | 12 + flutter/lib/mobile/pages/terminal_page.dart | 5 +- flutter/lib/models/terminal_model.dart | 20 +- src/server/terminal_service.rs | 484 +++++++++++++++--- 5 files changed, 449 insertions(+), 77 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index b941632dd..ab1b0b3c5 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -3063,6 +3063,11 @@ Future start_service(bool is_start) async { } Future canBeBlocked() async { + if (isWeb) { + // Web can only act as a controller, never as a controlled side, + // so it should never be blocked by a remote session. + return false; + } // First check control permission final controlPermission = await bind.mainGetCommon( key: "is-remote-modify-enabled-by-control-permissions"); diff --git a/flutter/lib/desktop/pages/terminal_tab_page.dart b/flutter/lib/desktop/pages/terminal_tab_page.dart index bc3ee1a8c..28e59fb05 100644 --- a/flutter/lib/desktop/pages/terminal_tab_page.dart +++ b/flutter/lib/desktop/pages/terminal_tab_page.dart @@ -36,6 +36,8 @@ class _TerminalTabPageState extends State { int _nextTerminalId = 1; // Lightweight idempotency guard for async close operations final Set _closingTabs = {}; + // When true, all session cleanup should persist (window-level close in progress) + bool _windowClosing = false; _TerminalTabPageState(Map params) { Get.put(DesktopTabController(tabType: DesktopTabType.terminal)); @@ -139,6 +141,7 @@ class _TerminalTabPageState extends State { /// UI tabs are removed immediately; session cleanup runs in parallel with a /// bounded timeout so window close is not blocked indefinitely. Future _closeAllTabs() async { + _windowClosing = true; final tabKeys = tabController.state.value.tabs.map((t) => t.key).toList(); // Remove all UI tabs immediately (same instant behavior as the old tabController.clear()) tabController.clear(); @@ -171,8 +174,17 @@ class _TerminalTabPageState extends State { /// - `true` (window close): persist all sessions, don't close any. /// - `false` (tab close): only persist the last session for the peer, /// close others so only the most recent disconnected session survives. + /// + /// Note: if [_windowClosing] is true, persistAll is forced to true so that + /// in-flight _closeTab() calls don't accidentally close sessions that the + /// window-close flow intends to preserve. Future _closeTerminalSessionIfNeeded(String tabKey, {bool persistAll = false, int? peerTabCount}) async { + // If window close is in progress, override to persist all sessions + // even if this call originated from an individual tab close. + if (_windowClosing) { + persistAll = true; + } final parsed = _parseTabKey(tabKey); if (parsed == null) return; final (peerId, terminalId) = parsed; diff --git a/flutter/lib/mobile/pages/terminal_page.dart b/flutter/lib/mobile/pages/terminal_page.dart index ab34a35ec..aff85b40c 100644 --- a/flutter/lib/mobile/pages/terminal_page.dart +++ b/flutter/lib/mobile/pages/terminal_page.dart @@ -83,7 +83,10 @@ class _TerminalPageState extends State // Register this terminal model with FFI for event routing _ffi.registerTerminalModel(widget.terminalId, _terminalModel); - _showTerminalExtraKeys = mainGetLocalBoolOptionSync(kOptionEnableShowTerminalExtraKeys); + // Web desktop users have full hardware keyboard access, so the on-screen + // terminal extra keys bar is unnecessary and disabled. + _showTerminalExtraKeys = !isWebDesktop && + mainGetLocalBoolOptionSync(kOptionEnableShowTerminalExtraKeys); // Initialize terminal connection WidgetsBinding.instance.addPostFrameCallback((_) { _ffi.dialogManager diff --git a/flutter/lib/models/terminal_model.dart b/flutter/lib/models/terminal_model.dart index 764528ab6..a74241ccb 100644 --- a/flutter/lib/models/terminal_model.dart +++ b/flutter/lib/models/terminal_model.dart @@ -266,8 +266,8 @@ class TerminalModel with ChangeNotifier { void _handleTerminalOpened(Map evt) { final bool success = getSuccessFromEvt(evt); - final String message = evt['message'] ?? ''; - final String? serviceId = evt['service_id']; + final String message = evt['message']?.toString() ?? ''; + final String? serviceId = evt['service_id']?.toString(); debugPrint( '[TerminalModel] Terminal opened response: success=$success, message=$message, service_id=$serviceId'); @@ -275,7 +275,18 @@ class TerminalModel with ChangeNotifier { if (success) { _terminalOpened = true; - // Service ID is now saved on the Rust side in handle_terminal_response + // On reconnect ("Reconnected to existing terminal"), server may replay recent output. + // If this TerminalView instance is reused (not rebuilt), duplicate lines can appear. + // We intentionally accept this tradeoff for now to keep logic simple. + + // Fallback: if terminal view is not yet ready but already has valid + // dimensions (e.g. layout completed before open response arrived), + // mark view ready now to avoid output stuck in buffer indefinitely. + if (!_terminalViewReady && + terminal.viewWidth > 0 && + terminal.viewHeight > 0) { + _markViewReady(); + } // Process any buffered input _processBufferedInputAsync().then((_) { @@ -358,8 +369,7 @@ class TerminalModel with ChangeNotifier { // because it only affects the pre-layout buffering window and the // terminal will self-correct on subsequent output. if (text.length >= _kMaxOutputBufferChars) { - final truncated = - text.substring(text.length - _kMaxOutputBufferChars); + final truncated = text.substring(text.length - _kMaxOutputBufferChars); _pendingOutputChunks ..clear() ..add(truncated); diff --git a/src/server/terminal_service.rs b/src/server/terminal_service.rs index ed7d02f68..fb6b4fd29 100644 --- a/src/server/terminal_service.rs +++ b/src/server/terminal_service.rs @@ -30,8 +30,54 @@ const MAX_OUTPUT_BUFFER_SIZE: usize = 1024 * 1024; // 1MB per terminal const MAX_BUFFER_LINES: usize = 10000; const MAX_SERVICES: usize = 100; // Maximum number of persistent terminal services const SERVICE_IDLE_TIMEOUT: Duration = Duration::from_secs(3600); // 1 hour idle timeout -const CHANNEL_BUFFER_SIZE: usize = 100; // Number of messages to buffer in channel +const CHANNEL_BUFFER_SIZE: usize = 500; // Channel buffer size. Max per-message size ~4KB (reader buffer), so worst case ~500*4KB ≈ 2MB/terminal. Increased from 100 to reduce data loss during disconnects. const COMPRESS_THRESHOLD: usize = 512; // Compress terminal data larger than this + // Default max bytes for reconnection buffer replay. +const DEFAULT_RECONNECT_BUFFER_BYTES: usize = 8 * 1024; +const MAX_SIGWINCH_PHASE_ATTEMPTS: u8 = 3; // Max attempts per SIGWINCH phase before giving up + +/// Two-phase SIGWINCH trigger for TUI app redraw on reconnection. +/// +/// Why two phases? A single resize-then-restore done back-to-back is too fast: +/// by the time the TUI app handles the asynchronous SIGWINCH signal and calls +/// `ioctl(TIOCGWINSZ)`, the PTY size has already been restored to the original. +/// ncurses sees no size change and skips the full redraw. +/// +/// Splitting across two `read_outputs()` calls (~30ms apart) ensures the app +/// sees a real size change on each SIGWINCH, forcing a complete redraw. +#[derive(Debug, Clone)] +enum SigwinchPhase { + /// No SIGWINCH needed. + Idle, + /// Phase 1: Resize PTY to temp dimensions (rows±1). The app handles SIGWINCH + /// and redraws at the temporary size. + TempResize { retries: u8 }, + /// Phase 2: Restore PTY to correct dimensions. The app handles SIGWINCH, + /// detects the size change, and performs a full redraw at the correct size. + Restore { retries: u8 }, +} + +/// Which resize to perform in the two-phase SIGWINCH sequence. +enum SigwinchAction { + /// Phase 1: resize to temp dimensions (rows±1) to trigger SIGWINCH with a visible size change. + TempResize, + /// Phase 2: restore to correct dimensions to trigger SIGWINCH and force full redraw. + Restore, +} + +/// Session state machine for terminal streaming. +#[derive(Debug)] +enum SessionState { + /// Session is closed, not streaming data to client. + Closed, + /// Session is active, streaming data to client. + /// pending_buffer: historical buffer to send before real-time data (set on reconnection). + /// sigwinch: two-phase SIGWINCH trigger state for TUI app redraw. + Active { + pending_buffer: Option>, + sigwinch: SigwinchPhase, + }, +} lazy_static::lazy_static! { // Global registry of persistent terminal services indexed by service_id @@ -433,22 +479,103 @@ impl OutputBuffer { } fn get_recent(&self, max_bytes: usize) -> Vec { - let mut result = Vec::new(); + if max_bytes == 0 { + return Vec::new(); + } + let mut chunks: Vec<&[u8]> = Vec::new(); let mut size = 0; - // Get recent lines up to max_bytes + // Collect whole chunks from newest to oldest, preserving chronological continuity. + // If the newest chunk alone exceeds max_bytes, take its tail (truncation may split + // an ANSI escape, but the terminal will self-correct on subsequent output). for line in self.lines.iter().rev() { if size + line.len() > max_bytes { + if size == 0 && line.len() > max_bytes { + // Single oversized chunk: take the tail to preserve the most recent content. + // Align offset forward to a UTF-8 char boundary so that downstream + // clients (e.g. Dart) that decode the payload as UTF-8 text don't + // encounter split code points. The protobuf bytes field itself allows + // arbitrary bytes; this is a best-effort mitigation for client-side decoding. + let mut offset = line.len() - max_bytes; + // Skip at most 3 continuation bytes (UTF-8 max 4-byte sequence). + // Prevents runaway skipping on non-UTF-8 binary data. + let mut skipped = 0u8; + while skipped < 3 + && offset < line.len() + && (line[offset] & 0b1100_0000) == 0b1000_0000 + { + offset += 1; + skipped += 1; + } + // If we skipped past all remaining bytes (degenerate data), drop the + // chunk entirely rather than emitting a slice that decodes poorly on the client. + if offset < line.len() { + chunks.push(&line[offset..]); + size = line.len() - offset; + } + } break; } size += line.len(); - result.splice(0..0, line.iter().cloned()); + chunks.push(line); + } + + // Reverse to restore chronological order and concatenate + chunks.reverse(); + let mut result = Vec::with_capacity(size); + for chunk in chunks { + result.extend_from_slice(chunk); } result } } +/// Try to send data through the output channel with rate-limited drop logging. +/// Returns `true` if the caller should break out of the read loop (channel disconnected). +fn try_send_output( + output_tx: &mpsc::SyncSender>, + data: Vec, + terminal_id: i32, + label: &str, + drop_count: &mut u64, + last_drop_warn: &mut Instant, +) -> bool { + match output_tx.try_send(data) { + Ok(_) => { + if *drop_count > 0 { + log::trace!( + "Terminal {}{} output channel recovered, dropped {} chunks since last report", + terminal_id, + label, + *drop_count + ); + *drop_count = 0; + } + false + } + Err(mpsc::TrySendError::Full(_)) => { + *drop_count += 1; + if last_drop_warn.elapsed() >= Duration::from_secs(5) { + log::trace!( + "Terminal {}{} output channel full, dropped {} chunks in last {:?}", + terminal_id, + label, + *drop_count, + last_drop_warn.elapsed() + ); + *drop_count = 0; + *last_drop_warn = Instant::now(); + } + false + } + Err(mpsc::TrySendError::Disconnected(_)) => { + log::debug!("Terminal {}{} output channel disconnected", terminal_id, label); + true + } + } +} + pub struct TerminalSession { pub created_at: Instant, last_activity: Instant, @@ -469,7 +596,8 @@ pub struct TerminalSession { cols: u16, // Track if we've already sent the closed message closed_message_sent: bool, - is_opened: bool, + // Session state machine for reconnection handling + state: SessionState, // Helper mode: PTY is managed by helper process, communication via message protocol #[cfg(target_os = "windows")] is_helper_mode: bool, @@ -496,7 +624,7 @@ impl TerminalSession { rows, cols, closed_message_sent: false, - is_opened: false, + state: SessionState::Closed, #[cfg(target_os = "windows")] is_helper_mode: false, #[cfg(target_os = "windows")] @@ -511,7 +639,7 @@ impl TerminalSession { // This helper function is to ensure that the threads are joined before the child process is dropped. // Though this is not strictly necessary on macOS. fn stop(&mut self) { - self.is_opened = false; + self.state = SessionState::Closed; self.exiting.store(true, Ordering::SeqCst); // Drop the input channel to signal writer thread to exit @@ -668,7 +796,9 @@ impl PersistentTerminalService { ( session.rows, session.cols, - session.output_buffer.get_recent(4096), + session + .output_buffer + .get_recent(DEFAULT_RECONNECT_BUFFER_BYTES), ) }) } @@ -683,7 +813,7 @@ impl PersistentTerminalService { self.needs_session_sync = true; for session in self.sessions.values() { let mut session = session.lock().unwrap(); - session.is_opened = false; + session.state = SessionState::Closed; } } } @@ -807,7 +937,25 @@ impl TerminalServiceProxy { if let Some(session_arc) = service.sessions.get(&open.terminal_id) { // Reconnect to existing terminal let mut session = session_arc.lock().unwrap(); - session.is_opened = true; + // Directly enter Active state with pending buffer for immediate streaming. + // Historical buffer is sent first by read_outputs(), then real-time data follows. + // No overlap: pending_buffer comes from output_buffer (pre-disconnect history), + // while received_data in read_outputs() comes from the channel (post-reconnect). + // During disconnect, the run loop (sp.ok()) exits so read_outputs() stops being + // called; output_buffer is not updated, and channel data may be lost if it fills up. + let buffer = session + .output_buffer + .get_recent(DEFAULT_RECONNECT_BUFFER_BYTES); + let has_pending = !buffer.is_empty(); + session.state = SessionState::Active { + pending_buffer: if has_pending { Some(buffer) } else { None }, + // Always trigger two-phase SIGWINCH on reconnect to force TUI app redraw, + // regardless of whether there's pending buffer data. This avoids edge cases + // where buffer is empty but a TUI app (top/htop) still needs a full redraw. + sigwinch: SigwinchPhase::TempResize { + retries: MAX_SIGWINCH_PHASE_ATTEMPTS, + }, + }; let mut opened = TerminalOpened::new(); opened.terminal_id = open.terminal_id; opened.success = true; @@ -829,13 +977,6 @@ impl TerminalServiceProxy { } response.set_opened(opened); - // Send buffered output - let buffer = session.output_buffer.get_recent(4096); - if !buffer.is_empty() { - // We'll need to send this separately or extend the protocol - // For now, just acknowledge the reconnection - } - return Ok(Some(response)); } @@ -945,6 +1086,9 @@ impl TerminalServiceProxy { let reader_thread = thread::spawn(move || { let mut reader = reader; let mut buf = vec![0u8; 4096]; + let mut drop_count: u64 = 0; + // Initialize to > 5s ago so the first drop triggers a warning immediately. + let mut last_drop_warn = Instant::now() - Duration::from_secs(6); loop { match reader.read(&mut buf) { Ok(0) => { @@ -958,19 +1102,22 @@ impl TerminalServiceProxy { break; } let data = buf[..n].to_vec(); - // Try to send, if channel is full, drop the data - match output_tx.try_send(data) { - Ok(_) => {} - Err(mpsc::TrySendError::Full(_)) => { - log::debug!( - "Terminal {} output channel full, dropping data", - terminal_id - ); - } - Err(mpsc::TrySendError::Disconnected(_)) => { - log::debug!("Terminal {} output channel disconnected", terminal_id); - break; - } + // Use try_send to avoid blocking the reader thread when channel is full. + // During disconnect, the run loop (sp.ok()) stops and read_outputs() is + // no longer called, so the channel won't be drained. Blocking send would + // deadlock the reader thread in that case. + // Note: data produced during disconnect may be lost if channel fills up, + // since output_buffer is only updated in read_outputs(). The buffer will + // contain history from before the disconnect, not data produced after it. + if try_send_output( + &output_tx, + data, + terminal_id, + "", + &mut drop_count, + &mut last_drop_warn, + ) { + break; } } Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { @@ -996,7 +1143,10 @@ impl TerminalServiceProxy { session.output_rx = Some(output_rx); session.reader_thread = Some(reader_thread); session.writer_thread = Some(writer_thread); - session.is_opened = true; + session.state = SessionState::Active { + pending_buffer: None, + sigwinch: SigwinchPhase::Idle, + }; let mut opened = TerminalOpened::new(); opened.terminal_id = open.terminal_id; @@ -1158,6 +1308,9 @@ impl TerminalServiceProxy { let terminal_id = open.terminal_id; let reader_thread = thread::spawn(move || { let mut buf = vec![0u8; 4096]; + let mut drop_count: u64 = 0; + // Initialize to > 5s ago so the first drop triggers a warning immediately. + let mut last_drop_warn = Instant::now() - Duration::from_secs(6); loop { match output_pipe.read(&mut buf) { Ok(0) => { @@ -1170,18 +1323,16 @@ impl TerminalServiceProxy { break; } let data = buf[..n].to_vec(); - match output_tx.try_send(data) { - Ok(_) => {} - Err(mpsc::TrySendError::Full(_)) => { - log::debug!( - "Terminal {} output channel full, dropping data", - terminal_id - ); - } - Err(mpsc::TrySendError::Disconnected(_)) => { - log::debug!("Terminal {} output channel disconnected", terminal_id); - break; - } + // Use try_send to avoid blocking the reader thread (same as direct PTY mode) + if try_send_output( + &output_tx, + data, + terminal_id, + " (helper)", + &mut drop_count, + &mut last_drop_warn, + ) { + break; } } Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { @@ -1211,7 +1362,10 @@ impl TerminalServiceProxy { session.output_rx = Some(output_rx); session.reader_thread = Some(reader_thread); session.writer_thread = Some(writer_thread); - session.is_opened = true; + session.state = SessionState::Active { + pending_buffer: None, + sigwinch: SigwinchPhase::Idle, + }; session.is_helper_mode = true; session.helper_process_handle = Some(SendableHandle::new(helper_raw_handle)); @@ -1253,6 +1407,11 @@ impl TerminalServiceProxy { session.rows = resize.rows as u16; session.cols = resize.cols as u16; + // Note: we do NOT clear the sigwinch phase here. The server-side two-phase + // SIGWINCH mechanism in read_outputs() is self-contained (temp resize → restore + // across two polling cycles), so client resize is purely a dimension sync and + // doesn't affect it. + // Windows: handle helper mode vs direct PTY mode #[cfg(target_os = "windows")] { @@ -1358,6 +1517,116 @@ impl TerminalServiceProxy { } } + /// Perform a single PTY resize as part of the two-phase SIGWINCH sequence. + /// Returns true if the resize succeeded. + /// + /// Takes individual field references to avoid borrowing the entire TerminalSession, + /// which would conflict with the mutable borrow of session.state in read_outputs(). + fn do_sigwinch_resize( + terminal_id: i32, + rows: u16, + cols: u16, + pty_pair: &Option, + input_tx: &Option>>, + _is_helper_mode: bool, + action: &SigwinchAction, + ) -> bool { + // Skip if dimensions are not initialized (shouldn't happen on reconnect, + // but guard against it to avoid resizing to nonsensical values). + if rows == 0 || cols == 0 { + return false; + } + + let target_rows = match action { + SigwinchAction::TempResize => { + // For very small terminals (≤2 rows), subtracting 1 would result in an unusable + // size (0 or 1 row), so we add 1 instead. Either direction triggers SIGWINCH. + if rows > 2 { + rows.saturating_sub(1) + } else { + rows.saturating_add(1) + } + } + SigwinchAction::Restore => rows, + }; + + let phase_name = match action { + SigwinchAction::TempResize => "temp resize", + SigwinchAction::Restore => "restore", + }; + + #[cfg(target_os = "windows")] + let use_helper = _is_helper_mode; + #[cfg(not(target_os = "windows"))] + let use_helper = false; + + if use_helper { + #[cfg(target_os = "windows")] + { + let input_tx = match input_tx { + Some(tx) => tx, + None => return false, + }; + let msg = encode_resize_message(target_rows, cols); + if let Err(e) = input_tx.try_send(msg) { + log::warn!( + "Terminal {} SIGWINCH {} via helper failed: {}", + terminal_id, + phase_name, + e + ); + return false; + } + true + } + #[cfg(not(target_os = "windows"))] + { + let _ = (input_tx, phase_name); + false + } + } else if let Some(pty_pair) = pty_pair { + if let Err(e) = pty_pair.master.resize(PtySize { + rows: target_rows, + cols, + pixel_width: 0, + pixel_height: 0, + }) { + log::warn!( + "Terminal {} SIGWINCH {} failed: {}", + terminal_id, + phase_name, + e + ); + return false; + } + true + } else { + false + } + } + + /// Helper to create a TerminalResponse with optional compression. + fn create_terminal_data_response(terminal_id: i32, data: Vec) -> TerminalResponse { + let mut response = TerminalResponse::new(); + let mut terminal_data = TerminalData::new(); + terminal_data.terminal_id = terminal_id; + + if data.len() > COMPRESS_THRESHOLD { + let compressed = compress::compress(&data); + if compressed.len() < data.len() { + terminal_data.data = bytes::Bytes::from(compressed); + terminal_data.compressed = true; + } else { + terminal_data.data = bytes::Bytes::from(data); + } + } else { + terminal_data.data = bytes::Bytes::from(data); + } + + response.set_data(terminal_data); + response + } + pub fn read_outputs(&self) -> Vec { let service = match get_service(&self.service_id) { Some(s) => s, @@ -1399,12 +1668,11 @@ impl TerminalServiceProxy { closed_terminals.push(terminal_id); } - if !session.is_opened { - // Skip the session if it is not opened. - continue; - } - - // Read from output channel + // Always drain the output channel regardless of session state. + // When Active: data is sent to client. When Closed (within the same + // connection): data is buffered in output_buffer for reconnection replay. + // Note: during actual disconnect, the run loop exits and read_outputs() + // is not called, so channel data produced after disconnect may be lost. let mut has_activity = false; let mut received_data = Vec::new(); if let Some(output_rx) = &session.output_rx { @@ -1415,37 +1683,111 @@ impl TerminalServiceProxy { } } - // Update buffer after reading + if has_activity { + session.update_activity(); + } + + // Update buffer (always buffer for reconnection support) for data in &received_data { session.output_buffer.append(data); } - // Process received data for responses - for data in received_data { - let mut response = TerminalResponse::new(); - let mut terminal_data = TerminalData::new(); - terminal_data.terminal_id = terminal_id; + // Skip sending responses if session is not Active. + // Data is already buffered above and will be sent on next reconnection. + // Use a scoped block to limit the mutable borrow of session.state, + // so we can immutably borrow other session fields afterwards. + let sigwinch_action = { + let (pending_buffer, sigwinch) = match &mut session.state { + SessionState::Active { + pending_buffer, + sigwinch, + } => (pending_buffer, sigwinch), + _ => continue, + }; - // Compress data if it exceeds threshold - if data.len() > COMPRESS_THRESHOLD { - let compressed = compress::compress(&data); - if compressed.len() < data.len() { - terminal_data.data = bytes::Bytes::from(compressed); - terminal_data.compressed = true; - } else { - // Compression didn't help, send uncompressed - terminal_data.data = bytes::Bytes::from(data); + // Send pending buffer response first (set on reconnection in handle_open). + // This ensures historical buffer is sent before any real-time data. + if let Some(buffer) = pending_buffer.take() { + if !buffer.is_empty() { + responses + .push(Self::create_terminal_data_response(terminal_id, buffer)); } - } else { - terminal_data.data = bytes::Bytes::from(data); } - response.set_data(terminal_data); - responses.push(response); + // Two-phase SIGWINCH: see SigwinchPhase doc comments for rationale. + // Each phase is a single PTY resize, spaced ~30ms apart by the polling + // interval, ensuring the TUI app sees a real size change on each signal. + match sigwinch { + SigwinchPhase::TempResize { retries } => { + if *retries == 0 { + log::warn!( + "Terminal {} SIGWINCH phase 1 (temp resize) failed after {} attempts, giving up", + terminal_id, MAX_SIGWINCH_PHASE_ATTEMPTS + ); + *sigwinch = SigwinchPhase::Idle; + None + } else { + *retries -= 1; + Some(SigwinchAction::TempResize) + } + } + SigwinchPhase::Restore { retries } => { + if *retries == 0 { + log::warn!( + "Terminal {} SIGWINCH phase 2 (restore) failed after {} attempts, giving up", + terminal_id, MAX_SIGWINCH_PHASE_ATTEMPTS + ); + *sigwinch = SigwinchPhase::Idle; + None + } else { + *retries -= 1; + Some(SigwinchAction::Restore) + } + } + SigwinchPhase::Idle => None, + } + }; + + // Execute SIGWINCH resize outside the mutable borrow scope of session.state. + if let Some(action) = sigwinch_action { + #[cfg(target_os = "windows")] + let is_helper = session.is_helper_mode; + #[cfg(not(target_os = "windows"))] + let is_helper = false; + let resize_ok = Self::do_sigwinch_resize( + terminal_id, + session.rows, + session.cols, + &session.pty_pair, + &session.input_tx, + is_helper, + &action, + ); + if let SessionState::Active { sigwinch, .. } = &mut session.state { + match action { + SigwinchAction::TempResize => { + if resize_ok { + // Phase 1 succeeded — advance to phase 2 (restore). + *sigwinch = SigwinchPhase::Restore { + retries: MAX_SIGWINCH_PHASE_ATTEMPTS, + }; + } + // If failed, retries already decremented; will retry phase 1. + } + SigwinchAction::Restore => { + if resize_ok { + // Phase 2 succeeded — SIGWINCH sequence complete. + *sigwinch = SigwinchPhase::Idle; + } + // If failed, retries already decremented; will retry phase 2. + } + } + } } - if has_activity { - session.update_activity(); + // Send real-time data after historical buffer + for data in received_data { + responses.push(Self::create_terminal_data_response(terminal_id, data)); } } } From eb239501bc67d03336e724fde62aafd583762c84 Mon Sep 17 00:00:00 2001 From: Amirhosein Akhlaghpoor Date: Tue, 24 Feb 2026 13:14:18 +0000 Subject: [PATCH 145/277] Fix logon-screen password with click approval (#14335) --- src/platform/windows.cc | 25 +++++++++++++++++++++++++ src/platform/windows.rs | 5 +++++ src/server/connection.rs | 7 ++++--- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/platform/windows.cc b/src/platform/windows.cc index d83a1b0c4..74c20c80d 100644 --- a/src/platform/windows.cc +++ b/src/platform/windows.cc @@ -580,6 +580,31 @@ extern "C" return rdp_or_console; } + BOOL is_session_locked(BOOL include_rdp) + { + DWORD session_id = get_current_session(include_rdp); + if (session_id == 0xFFFFFFFF) { + return FALSE; + } + PWTSINFOEXW pInfo = NULL; + DWORD bytes = 0; + BOOL locked = FALSE; + if (WTSQuerySessionInformationW( + WTS_CURRENT_SERVER_HANDLE, + session_id, + WTSSessionInfoEx, + (LPWSTR *)&pInfo, + &bytes)) { + if (pInfo && pInfo->Level == 1) { + locked = (pInfo->Data.WTSInfoExLevel1.SessionFlags == WTS_SESSIONSTATE_LOCK); + } + if (pInfo) { + WTSFreeMemory(pInfo); + } + } + return locked; + } + uint32_t get_active_user(PWSTR bufin, uint32_t nin, BOOL rdp) { uint32_t nout = 0; diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 582451240..a45220eb4 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -527,6 +527,7 @@ const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS; extern "C" { fn get_current_session(rdp: BOOL) -> DWORD; + fn is_session_locked(include_rdp: BOOL) -> BOOL; fn LaunchProcessWin( cmd: *const u16, session_id: DWORD, @@ -1129,6 +1130,10 @@ pub fn is_prelogin() -> bool { username.is_empty() || username == "SYSTEM" } +pub fn is_locked() -> bool { + unsafe { is_session_locked(share_rdp()) == TRUE } +} + // `is_logon_ui()` is regardless of multiple sessions now. // It only check if "LogonUI.exe" exists. // diff --git a/src/server/connection.rs b/src/server/connection.rs index 10b578042..1259054cd 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -2232,11 +2232,12 @@ impl Connection { // https://github.com/rustdesk/rustdesk-server-pro/discussions/646 // `is_logon` is used to check login with `OPTION_ALLOW_LOGON_SCREEN_PASSWORD` == "Y". - // `is_logon_ui()` is used on Windows, because there's no good way to detect `is_locked()`. - // Detecting `is_logon_ui()` (if `LogonUI.exe` running) is a workaround. + // `is_logon_ui()` is a fallback for logon UI detection on Windows. #[cfg(target_os = "windows")] let is_logon = || { - crate::platform::is_prelogin() || { + crate::platform::is_prelogin() + || crate::platform::is_locked() + || { match crate::platform::is_logon_ui() { Ok(result) => result, Err(e) => { From dc760d6ca84a48374ab98b770a8eaabaea2ee290 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 24 Feb 2026 21:52:26 +0800 Subject: [PATCH 146/277] remove .claude --- .claude/commands/reflection.md | 56 ---------------------------------- 1 file changed, 56 deletions(-) delete mode 100644 .claude/commands/reflection.md diff --git a/.claude/commands/reflection.md b/.claude/commands/reflection.md deleted file mode 100644 index 9628fc157..000000000 --- a/.claude/commands/reflection.md +++ /dev/null @@ -1,56 +0,0 @@ -You are an expert in prompt engineering, specializing in optimizing AI code assistant instructions. Your task is to analyze and improve the instructions for Claude Code. -Follow these steps carefully: - -1. Analysis Phase: -Review the chat history in your context window. - -Then, examine the current Claude instructions, commands and config - -/CLAUDE.md -/.claude/commands/* -**/CLAUDE.md -.claude/settings.json -.claude/settings.local.json - - -Analyze the chat history, instructions, commands and config to identify areas that could be improved. Look for: -- Inconsistencies in Claude's responses -- Misunderstandings of user requests -- Areas where Claude could provide more detailed or accurate information -- Opportunities to enhance Claude's ability to handle specific types of queries or tasks -- New commands or improvements to a commands name, function or response -- Permissions and MCPs we've approved locally that we should add to the config, especially if we've added new tools or require them for the command to work - -2. Interaction Phase: -Present your findings and improvement ideas to the human. For each suggestion: -a) Explain the current issue you've identified -b) Propose a specific change or addition to the instructions -c) Describe how this change would improve Claude's performance - -Wait for feedback from the human on each suggestion before proceeding. If the human approves a change, move it to the implementation phase. If not, refine your suggestion or move on to the next idea. - -3. Implementation Phase: -For each approved change: -a) Clearly state the section of the instructions you're modifying -b) Present the new or modified text for that section -c) Explain how this change addresses the issue identified in the analysis phase - -4. Output Format: -Present your final output in the following structure: - - -[List the issues identified and potential improvements] - - - -[For each approved improvement: -1. Section being modified -2. New or modified instruction text -3. Explanation of how this addresses the identified issue] - - - -[Present the complete, updated set of instructions for Claude, incorporating all approved changes] - - -Remember, your goal is to enhance Claude's performance and consistency while maintaining the core functionality and purpose of the AI assistant. Be thorough in your analysis, clear in your explanations, and precise in your implementations. \ No newline at end of file From 82a9fd15404368c43824c55f65b5e2c79fda9293 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 24 Feb 2026 21:57:55 +0800 Subject: [PATCH 147/277] change port forward listen to localhost --- src/port_forward.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/port_forward.rs b/src/port_forward.rs index 056233b00..61d6bfd71 100644 --- a/src/port_forward.rs +++ b/src/port_forward.rs @@ -54,7 +54,7 @@ pub async fn listen( remote_host: String, remote_port: i32, ) -> ResultType<()> { - let listener = tcp::new_listener(format!("0.0.0.0:{}", port), true).await?; + let listener = tcp::new_listener(format!("127.0.0.1:{}", port), true).await?; let addr = listener.local_addr()?; log::info!("listening on port {:?}", addr); let is_rdp = port == 0; From 6aee70fa18f4ac599411c8a1391eb2e0ed836d41 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Wed, 25 Feb 2026 17:09:51 +0800 Subject: [PATCH 148/277] fix https://github.com/rustdesk/rustdesk/issues/609#issuecomment-3931613118 (#14364) --- src/ui/chatbox.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ui/chatbox.html b/src/ui/chatbox.html index 10d85a567..87d616289 100644 --- a/src/ui/chatbox.html +++ b/src/ui/chatbox.html @@ -12,7 +12,11 @@ include "common.tis"; var p = view.parameters; view.refresh = function() { + var draft_input = $(input); + var draft = draft_input ? (draft_input.value || "") : ""; $(body).content(); + var next_input = $(input); + if (next_input) next_input.value = draft; view.focus = $(input); } function self.closing() { From 3cc331508199a8bedb7176c97bae45f25c0a92d4 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Thu, 26 Feb 2026 11:27:58 +0100 Subject: [PATCH 149/277] Update de.rs (#14385) --- src/lang/de.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index a518dd3c3..03e501848 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -739,6 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", "Bildschirm während ausgehender Sitzungen aktiv halten"), ("keep-awake-during-incoming-sessions-label", "Bildschirm während eingehender Sitzungen aktiv halten"), ("Continue with {}", "Fortfahren mit {}"), - ("Display Name", ""), + ("Display Name", "Anzeigename"), ].iter().cloned().collect(); } From fd431844068ed79d526762e7518d8ba47892108e Mon Sep 17 00:00:00 2001 From: solokot Date: Thu, 26 Feb 2026 13:28:09 +0300 Subject: [PATCH 150/277] Update ru.rs (#14386) --- src/lang/ru.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 344260d34..35114efe3 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -739,6 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", "Не отключать экран во время исходящих сеансов"), ("keep-awake-during-incoming-sessions-label", "Не отключать экран во время входящих сеансов"), ("Continue with {}", "Продолжить с {}"), - ("Display Name", ""), + ("Display Name", "Отображаемое имя"), ].iter().cloned().collect(); } From 34803f8e9bc625a2ceaba4cfe627713b44468c6a Mon Sep 17 00:00:00 2001 From: memory_clear <83893503+MemoryClear@users.noreply.github.com> Date: Thu, 26 Feb 2026 18:28:19 +0800 Subject: [PATCH 151/277] Update labels for keep awake during sessions (#14391) --- src/lang/cn.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 5cb228a6e..0cc6aacd1 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -736,8 +736,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-exit-{}-tip", "按下 {} 退出"), ("rel-mouse-permission-lost-tip", "键盘权限被撤销。相对鼠标模式已被禁用。"), ("Changelog", "更新日志"), - ("keep-awake-during-outgoing-sessions-label", ""), - ("keep-awake-during-incoming-sessions-label", ""), + ("keep-awake-during-outgoing-sessions-label", "传出会话期间保持屏幕常亮"), + ("keep-awake-during-incoming-sessions-label", "传入会话期间保持屏幕常亮"), ("Continue with {}", "使用 {} 登录"), ("Display Name", "显示名称"), ].iter().cloned().collect(); From 12d6789c2ed9ba7a890085b80deae10a35bce53f Mon Sep 17 00:00:00 2001 From: Alex Rijckaert Date: Fri, 27 Feb 2026 05:09:47 +0100 Subject: [PATCH 152/277] Update translation (#14413) --- src/lang/nl.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 577f7487f..99b859248 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -739,6 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", "Houd het scherm open tijdens de uitgaande sessies."), ("keep-awake-during-incoming-sessions-label", "Houd het scherm open tijdens de inkomende sessies."), ("Continue with {}", "Ga verder met {}"), - ("Display Name", ""), + ("Display Name", "Naam Weergeven"), ].iter().cloned().collect(); } From 394079833efe8925af05ba7254095c5931f4b7bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?VenusGirl=E2=9D=A4?= Date: Fri, 27 Feb 2026 21:14:14 +0900 Subject: [PATCH 153/277] Update ko.rs (#14418) --- src/lang/ko.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 1e3d4f9b8..7230d1a1f 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -739,6 +739,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", "발신 세션 중 화면 켜짐 유지"), ("keep-awake-during-incoming-sessions-label", "수신 세션 중 화면 켜짐 유지"), ("Continue with {}", "{}(으)로 계속"), - ("Display Name", ""), + ("Display Name", "표시 이름"), ].iter().cloned().collect(); } From d49ae493b262cd876ac7be8a6fd4e65a518c9e17 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 27 Feb 2026 20:53:40 +0800 Subject: [PATCH 154/277] bump to 1.4.6 --- .github/workflows/flutter-build.yml | 2 +- .github/workflows/playground.yml | 2 +- .github/workflows/winget.yml | 4 ++-- Cargo.lock | 4 ++-- Cargo.toml | 2 +- appimage/AppImageBuilder-aarch64.yml | 2 +- appimage/AppImageBuilder-x86_64.yml | 2 +- flutter/pubspec.yaml | 2 +- libs/portable/Cargo.toml | 2 +- res/PKGBUILD | 2 +- res/rpm-flutter-suse.spec | 2 +- res/rpm-flutter.spec | 2 +- res/rpm.spec | 2 +- 13 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index 22b24d483..eb101400d 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -39,7 +39,7 @@ env: # 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`. VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b" ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" # 2025.01.13, got "/opt/artifacts/vcpkg/vcpkg: No such file or directory" with latest version - VERSION: "1.4.5" + VERSION: "1.4.6" NDK_VERSION: "r27c" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" diff --git a/.github/workflows/playground.yml b/.github/workflows/playground.yml index 0c7b450a3..110437e0f 100644 --- a/.github/workflows/playground.yml +++ b/.github/workflows/playground.yml @@ -17,7 +17,7 @@ env: TAG_NAME: "nightly" VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b" - VERSION: "1.4.5" + VERSION: "1.4.6" NDK_VERSION: "r26d" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml index ce54723e9..90a3d4fb3 100644 --- a/.github/workflows/winget.yml +++ b/.github/workflows/winget.yml @@ -10,6 +10,6 @@ jobs: - uses: vedantmgoyal9/winget-releaser@main with: identifier: RustDesk.RustDesk - version: "1.4.5" - release-tag: "1.4.5" + version: "1.4.6" + release-tag: "1.4.6" token: ${{ secrets.WINGET_TOKEN }} diff --git a/Cargo.lock b/Cargo.lock index 5aec38900..06cfeeb96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7134,7 +7134,7 @@ dependencies = [ [[package]] name = "rustdesk" -version = "1.4.5" +version = "1.4.6" dependencies = [ "android-wakelock", "android_logger", @@ -7249,7 +7249,7 @@ dependencies = [ [[package]] name = "rustdesk-portable-packer" -version = "1.4.5" +version = "1.4.6" dependencies = [ "brotli", "dirs 5.0.1", diff --git a/Cargo.toml b/Cargo.toml index ac1050bf7..d792d5cd5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk" -version = "1.4.5" +version = "1.4.6" authors = ["rustdesk "] edition = "2021" build= "build.rs" diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index d4af2d13a..64d6c2cfa 100644 --- a/appimage/AppImageBuilder-aarch64.yml +++ b/appimage/AppImageBuilder-aarch64.yml @@ -18,7 +18,7 @@ AppDir: id: rustdesk name: rustdesk icon: rustdesk - version: 1.4.5 + version: 1.4.6 exec: usr/share/rustdesk/rustdesk exec_args: $@ apt: diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index d85bd381e..933673cef 100644 --- a/appimage/AppImageBuilder-x86_64.yml +++ b/appimage/AppImageBuilder-x86_64.yml @@ -18,7 +18,7 @@ AppDir: id: rustdesk name: rustdesk icon: rustdesk - version: 1.4.5 + version: 1.4.6 exec: usr/share/rustdesk/rustdesk exec_args: $@ apt: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index b8360db58..eb6d76161 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers -version: 1.4.5+63 +version: 1.4.6+64 environment: sdk: '^3.1.0' diff --git a/libs/portable/Cargo.toml b/libs/portable/Cargo.toml index a4a71e14f..184079be8 100644 --- a/libs/portable/Cargo.toml +++ b/libs/portable/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk-portable-packer" -version = "1.4.5" +version = "1.4.6" edition = "2021" description = "RustDesk Remote Desktop" diff --git a/res/PKGBUILD b/res/PKGBUILD index 3b4096760..dd266eb2a 100644 --- a/res/PKGBUILD +++ b/res/PKGBUILD @@ -1,5 +1,5 @@ pkgname=rustdesk -pkgver=1.4.5 +pkgver=1.4.6 pkgrel=0 epoch= pkgdesc="" diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec index 2049b5f4f..bb2b56af6 100644 --- a/res/rpm-flutter-suse.spec +++ b/res/rpm-flutter-suse.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.4.5 +Version: 1.4.6 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec index f8bc7a1a1..1a077ee7e 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.4.5 +Version: 1.4.6 Release: 0 Summary: RPM package License: GPL-3.0 diff --git a/res/rpm.spec b/res/rpm.spec index 26c497121..6a7377b8b 100644 --- a/res/rpm.spec +++ b/res/rpm.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.4.5 +Version: 1.4.6 Release: 0 Summary: RPM package License: GPL-3.0 From 4abdb2e08bd84ccff59571996ba465a7209553fc Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 27 Feb 2026 21:50:20 +0800 Subject: [PATCH 155/277] feat: windows, custom client, update (#13687) Signed-off-by: fufesou --- .gitignore | 1 + flutter/lib/common.dart | 4 +- .../lib/desktop/pages/desktop_home_page.dart | 8 +- .../desktop/pages/desktop_setting_page.dart | 3 +- flutter/windows/runner/win32_window.cpp | 56 +- libs/hbb_common | 2 +- res/msi/CustomActions/CustomActions.cpp | 192 +++++- src/common.rs | 6 +- src/core_main.rs | 26 +- src/flutter_ffi.rs | 32 +- src/hbbs_http/downloader.rs | 43 +- src/platform/windows.rs | 651 ++++++++++++++++-- src/rendezvous_mediator.rs | 2 +- src/ui/index.tis | 4 +- src/updater.rs | 51 +- 15 files changed, 957 insertions(+), 124 deletions(-) diff --git a/.gitignore b/.gitignore index b4ea62660..d2e09a906 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .vscode .idea .DS_Store +.env libsciter-gtk.so src/ui/inline.rs extractor diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index ab1b0b3c5..ca52c61e0 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -3938,7 +3938,9 @@ void earlyAssert() { void checkUpdate() { if (!isWeb) { - if (!bind.isCustomClient()) { + final isWindowsInstalled = isWindows && bind.mainIsInstalled(); + final shouldCheckUpdate = isWindowsInstalled || !bind.isCustomClient(); + if (shouldCheckUpdate) { platformFFI.registerEventHandler( kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish, (Map evt) async { diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 339ecddb0..b9af2dc7b 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -430,10 +430,12 @@ class _DesktopHomePageState extends State } Widget buildHelpCards(String updateUrl) { - if (!bind.isCustomClient() && - updateUrl.isNotEmpty && + final isWindowsInstalled = isWindows && bind.mainIsInstalled(); + if (updateUrl.isNotEmpty && !isCardClosed && - bind.mainUriPrefixSync().contains('rustdesk')) { + (isWindowsInstalled || + (!bind.isCustomClient() && + bind.mainUriPrefixSync().contains('rustdesk')))) { final isToUpdate = (isWindows || isMacOS) && bind.mainIsInstalled(); String btnText = isToUpdate ? 'Update' : 'Download'; GestureTapCallback onPressed = () async { diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 3314d82ab..d8239adea 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -473,8 +473,7 @@ class _GeneralState extends State<_General> { } Widget other() { - final showAutoUpdate = - isWindows && bind.mainIsInstalled() && !bind.isCustomClient(); + final showAutoUpdate = isWindows && bind.mainIsInstalled(); final children = [ if (!isWeb && !bind.isIncomingOnly()) _OptionCheckBox(context, 'Confirm before closing multiple tabs', diff --git a/flutter/windows/runner/win32_window.cpp b/flutter/windows/runner/win32_window.cpp index 2c25f00dd..606ef0aa3 100644 --- a/flutter/windows/runner/win32_window.cpp +++ b/flutter/windows/runner/win32_window.cpp @@ -7,6 +7,7 @@ #include // for getenv and _putenv #include // for strcmp +#include // for std::wstring namespace { @@ -15,6 +16,43 @@ constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; // The number of Win32Window objects that currently exist. static int g_active_window_count = 0; +// Static variable to hold the custom icon (needs cleanup on exit) +static HICON g_custom_icon_ = nullptr; + +// Try to load icon from data\flutter_assets\assets\icon.ico if it exists. +// Returns nullptr if the file doesn't exist or can't be loaded. +HICON LoadCustomIcon() { + if (g_custom_icon_ != nullptr) { + return g_custom_icon_; + } + wchar_t exe_path[MAX_PATH]; + if (!GetModuleFileNameW(nullptr, exe_path, MAX_PATH)) { + return nullptr; + } + + std::wstring icon_path = exe_path; + size_t last_slash = icon_path.find_last_of(L"\\/"); + if (last_slash == std::wstring::npos) { + return nullptr; + } + + icon_path = icon_path.substr(0, last_slash + 1); + icon_path += L"data\\flutter_assets\\assets\\icon.ico"; + + // Check file attributes - reject if missing, directory, or reparse point (symlink/junction) + DWORD file_attr = GetFileAttributesW(icon_path.c_str()); + if (file_attr == INVALID_FILE_ATTRIBUTES || + (file_attr & FILE_ATTRIBUTE_DIRECTORY) || + (file_attr & FILE_ATTRIBUTE_REPARSE_POINT)) { + return nullptr; + } + + g_custom_icon_ = (HICON)LoadImageW( + nullptr, icon_path.c_str(), IMAGE_ICON, 0, 0, + LR_LOADFROMFILE | LR_DEFAULTSIZE); + return g_custom_icon_; +} + using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); // Scale helper to convert logical scaler values to physical using passed in @@ -81,8 +119,16 @@ const wchar_t* WindowClassRegistrar::GetWindowClass() { window_class.cbClsExtra = 0; window_class.cbWndExtra = 0; window_class.hInstance = GetModuleHandle(nullptr); - window_class.hIcon = - LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + + // Try to load icon from data\flutter_assets\assets\icon.ico if it exists + HICON custom_icon = LoadCustomIcon(); + if (custom_icon != nullptr) { + window_class.hIcon = custom_icon; + } else { + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + } + window_class.hbrBackground = 0; window_class.lpszMenuName = nullptr; window_class.lpfnWndProc = Win32Window::WndProc; @@ -95,6 +141,12 @@ const wchar_t* WindowClassRegistrar::GetWindowClass() { void WindowClassRegistrar::UnregisterWindowClass() { UnregisterClass(kWindowClassName, nullptr); class_registered_ = false; + + // Clean up the custom icon if it was loaded + if (g_custom_icon_ != nullptr) { + DestroyIcon(g_custom_icon_); + g_custom_icon_ = nullptr; + } } Win32Window::Win32Window() { diff --git a/libs/hbb_common b/libs/hbb_common index 0b60b9ffa..5e07db744 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 0b60b9ffa05259f72cd33e79010ef8e15d42b851 +Subproject commit 5e07db7444284006c008b5b1204f0968bc47b1a9 diff --git a/res/msi/CustomActions/CustomActions.cpp b/res/msi/CustomActions/CustomActions.cpp index fafbab6b5..f21cc7ee1 100644 --- a/res/msi/CustomActions/CustomActions.cpp +++ b/res/msi/CustomActions/CustomActions.cpp @@ -31,22 +31,168 @@ LExit: return WcaFinalize(er); } -// CAUTION: We can't simply remove the install folder here, because silent repair/upgrade will fail. -// `RemoveInstallFolder()` is a deferred custom action, it will be executed after the files are copied. -// `msiexec /i package.msi /qn` +// Helper function to safely delete a file or directory using handle-based deletion. +// This avoids TOCTOU (Time-Of-Check-Time-Of-Use) race conditions. +BOOL SafeDeleteItem(LPCWSTR fullPath) +{ + // Open the file/directory with DELETE access and FILE_FLAG_OPEN_REPARSE_POINT + // to prevent following symlinks. + // Use shared access to allow deletion even when other processes have the file open. + DWORD flags = FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT; + HANDLE hFile = CreateFileW( + fullPath, + DELETE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, // Allow shared access + NULL, + OPEN_EXISTING, + flags, + NULL + ); + + if (hFile == INVALID_HANDLE_VALUE) + { + WcaLog(LOGMSG_STANDARD, "SafeDeleteItem: Failed to open '%ls'. Error: %lu", fullPath, GetLastError()); + return FALSE; + } + + // Use SetFileInformationByHandle to mark for deletion. + // The file will be deleted when the handle is closed. + FILE_DISPOSITION_INFO dispInfo; + dispInfo.DeleteFile = TRUE; + + BOOL result = SetFileInformationByHandle( + hFile, + FileDispositionInfo, + &dispInfo, + sizeof(dispInfo) + ); + + if (!result) + { + DWORD error = GetLastError(); + WcaLog(LOGMSG_STANDARD, "SafeDeleteItem: Failed to mark '%ls' for deletion. Error: %lu", fullPath, error); + } + + CloseHandle(hFile); + return result; +} + +// Helper function to recursively delete a directory's contents with detailed logging. +void RecursiveDelete(LPCWSTR path) +{ + // Ensure the path is not empty or null. + if (path == NULL || path[0] == L'\0') + { + return; + } + + // Extra safety: never operate directly on a root path. + if (PathIsRootW(path)) + { + WcaLog(LOGMSG_STANDARD, "RecursiveDelete: refusing to operate on root path '%ls'.", path); + return; + } + + // MAX_PATH is enough here since the installer should not be using longer paths. + // No need to handle extended-length paths (\\?\) in this context. + WCHAR searchPath[MAX_PATH]; + HRESULT hr = StringCchPrintfW(searchPath, MAX_PATH, L"%s\\*", path); + if (FAILED(hr)) { + WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Path too long to enumerate: %ls", path); + return; + } + + WIN32_FIND_DATAW findData; + HANDLE hFind = FindFirstFileW(searchPath, &findData); + + if (hFind == INVALID_HANDLE_VALUE) + { + // This can happen if the directory is empty or doesn't exist, which is not an error in our case. + WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Failed to enumerate directory '%ls'. It may be missing or inaccessible. Error: %lu", path, GetLastError()); + return; + } + + do + { + // Skip '.' and '..' directories. + if (wcscmp(findData.cFileName, L".") == 0 || wcscmp(findData.cFileName, L"..") == 0) + { + continue; + } + + // MAX_PATH is enough here since the installer should not be using longer paths. + // No need to handle extended-length paths (\\?\) in this context. + WCHAR fullPath[MAX_PATH]; + hr = StringCchPrintfW(fullPath, MAX_PATH, L"%s\\%s", path, findData.cFileName); + if (FAILED(hr)) { + WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Path too long for item '%ls' in '%ls', skipping.", findData.cFileName, path); + continue; + } + + // Before acting, ensure the read-only attribute is not set. + if (findData.dwFileAttributes & FILE_ATTRIBUTE_READONLY) + { + if (FALSE == SetFileAttributesW(fullPath, findData.dwFileAttributes & ~FILE_ATTRIBUTE_READONLY)) + { + WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Failed to remove read-only attribute. Error: %lu", GetLastError()); + } + } + + if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + { + // Check for reparse points (symlinks/junctions) to prevent directory traversal attacks. + // Do not follow reparse points, only remove the link itself. + if (findData.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) + { + WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Not recursing into reparse point (symlink/junction), deleting link itself: %ls", fullPath); + SafeDeleteItem(fullPath); + } + else + { + // Recursively delete directory contents first + RecursiveDelete(fullPath); + // Then delete the directory itself + SafeDeleteItem(fullPath); + } + } + else + { + // Delete file using safe handle-based deletion + SafeDeleteItem(fullPath); + } + } while (FindNextFileW(hFind, &findData) != 0); + + DWORD lastError = GetLastError(); + if (lastError != ERROR_NO_MORE_FILES) + { + WcaLog(LOGMSG_STANDARD, "RecursiveDelete: FindNextFileW failed with error %lu", lastError); + } + + FindClose(hFind); +} + +// See `Package.wxs` for the sequence of this custom action. // -// So we need to delete the files separately in install folder. +// Upgrade/uninstall sequence: +// 1. InstallInitialize +// 2. RemoveExistingProducts +// ├─ TerminateProcesses +// ├─ TryStopDeleteService +// ├─ RemoveInstallFolder - <-- Here +// └─ RemoveFiles +// 3. InstallValidate +// 4. InstallFiles +// 5. InstallExecute +// 6. InstallFinalize UINT __stdcall RemoveInstallFolder( __in MSIHANDLE hInstall) { HRESULT hr = S_OK; DWORD er = ERROR_SUCCESS; - int nResult = 0; LPWSTR installFolder = NULL; LPWSTR pwz = NULL; LPWSTR pwzData = NULL; - WCHAR runtimeBroker[1024] = { 0, }; hr = WcaInitialize(hInstall, "RemoveInstallFolder"); ExitOnFailure(hr, "Failed to initialize"); @@ -58,24 +204,23 @@ UINT __stdcall RemoveInstallFolder( hr = WcaReadStringFromCaData(&pwz, &installFolder); ExitOnFailure(hr, "failed to read database key from custom action data: %ls", pwz); - StringCchPrintfW(runtimeBroker, sizeof(runtimeBroker) / sizeof(runtimeBroker[0]), L"%ls\\RuntimeBroker_rustdesk.exe", installFolder); - - SHFILEOPSTRUCTW fileOp; - ZeroMemory(&fileOp, sizeof(SHFILEOPSTRUCT)); - fileOp.wFunc = FO_DELETE; - fileOp.pFrom = runtimeBroker; - fileOp.fFlags = FOF_NOCONFIRMATION | FOF_SILENT; - - nResult = SHFileOperationW(&fileOp); - if (nResult == 0) - { - WcaLog(LOGMSG_STANDARD, "The external file \"%ls\" has been deleted.", runtimeBroker); + if (installFolder == NULL || installFolder[0] == L'\0') { + WcaLog(LOGMSG_STANDARD, "Install folder path is empty, skipping recursive delete."); + goto LExit; } - else - { - WcaLog(LOGMSG_STANDARD, "The external file \"%ls\" has not been deleted, error code: 0x%02X. Please refer to https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shfileoperationa for the error codes.", runtimeBroker, nResult); + + if (PathIsRootW(installFolder)) { + WcaLog(LOGMSG_STANDARD, "Refusing to recursively delete root folder '%ls'.", installFolder); + goto LExit; } + WcaLog(LOGMSG_STANDARD, "Attempting to recursively delete contents of install folder: %ls", installFolder); + + RecursiveDelete(installFolder); + + // The standard MSI 'RemoveFolders' action will take care of removing the (now empty) directories. + // We don't need to call RemoveDirectoryW on installFolder itself, as it might still be in use by the installer. + LExit: ReleaseStr(pwzData); @@ -109,9 +254,12 @@ bool TerminateProcessIfNotContainsParam(pfnNtQueryInformationProcess NtQueryInfo { if (pebUpp.CommandLine.Length > 0) { - WCHAR *commandLine = (WCHAR *)malloc(pebUpp.CommandLine.Length); + // Allocate extra space for null terminator + WCHAR *commandLine = (WCHAR *)malloc(pebUpp.CommandLine.Length + sizeof(WCHAR)); if (commandLine != NULL) { + // Initialize all bytes to zero for safety + memset(commandLine, 0, pebUpp.CommandLine.Length + sizeof(WCHAR)); if (ReadProcessMemory(process, pebUpp.CommandLine.Buffer, commandLine, pebUpp.CommandLine.Length, &dwBytesRead)) { diff --git a/src/common.rs b/src/common.rs index bba453c34..d2c252869 100644 --- a/src/common.rs +++ b/src/common.rs @@ -39,7 +39,7 @@ use hbb_common::{ use crate::{ hbbs_http::{create_http_client_async, get_url_for_tls}, - ui_interface::{get_option, set_option}, + ui_interface::{get_option, is_installed, set_option}, }; #[derive(Debug, Eq, PartialEq)] @@ -940,7 +940,9 @@ pub fn is_modifier(evt: &KeyEvent) -> bool { } pub fn check_software_update() { - if is_custom_client() { + let is_windows_installed = cfg!(target_os = "windows") && is_installed(); + let should_check_update = is_windows_installed || !is_custom_client(); + if !should_check_update { return; } let opt = LocalConfig::get_option(keys::OPTION_ENABLE_CHECK_UPDATE); diff --git a/src/core_main.rs b/src/core_main.rs index 7962a693e..3119529c6 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -187,7 +187,10 @@ pub fn core_main() -> Option> { } #[cfg(windows)] - hbb_common::config::PeerConfig::preload_peers(); + { + crate::platform::try_remove_temp_update_files(); + hbb_common::config::PeerConfig::preload_peers(); + } std::thread::spawn(move || crate::start_server(false, no_server)); } else { #[cfg(windows)] @@ -202,17 +205,24 @@ pub fn core_main() -> Option> { if config::is_disable_installation() { return None; } - let res = platform::update_me(false); - let text = match res { - Ok(_) => translate("Update successfully!".to_string()), - Err(err) => { - log::error!("Failed with error: {err}"); - translate("Update failed!".to_string()) + + let text = match crate::platform::prepare_custom_client_update() { + Err(e) => { + log::error!("Error preparing custom client update: {}", e); + "Update failed!".to_string() } + Ok(false) => "Update failed!".to_string(), + Ok(true) => match platform::update_me(false) { + Ok(_) => "Update successfully!".to_string(), + Err(err) => { + log::error!("Failed with error: {err}"); + "Update failed!".to_string() + } + }, }; Toast::new(Toast::POWERSHELL_APP_ID) .title(&config::APP_NAME.read().unwrap()) - .text1(&text) + .text1(&translate(text)) .sound(Some(Sound::Default)) .duration(Duration::Short) .show() diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 864002d24..ed13a7624 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -2776,10 +2776,13 @@ pub fn main_get_common(key: String) -> String { } else if key.starts_with("download-file-") { let _version = key.replace("download-file-", ""); #[cfg(target_os = "windows")] - return match crate::platform::windows::is_msi_installed() { - Ok(true) => format!("rustdesk-{_version}-x86_64.msi"), - Ok(false) => format!("rustdesk-{_version}-x86_64.exe"), - Err(e) => { + return match ( + crate::platform::windows::is_msi_installed(), + crate::common::is_custom_client(), + ) { + (Ok(true), false) => format!("rustdesk-{_version}-x86_64.msi"), + (Ok(true), true) | (Ok(false), _) => format!("rustdesk-{_version}-x86_64.exe"), + (Err(e), _) => { log::error!("Failed to check if is msi: {}", e); format!("error:update-failed-check-msi-tip") } @@ -2876,30 +2879,17 @@ pub fn main_set_common(_key: String, _value: String) { if let Some(f) = new_version_file.to_str() { // 1.4.0 does not support "--update" // But we can assume that the new version supports it. - #[cfg(target_os = "windows")] - if f.ends_with(".exe") { - if let Err(e) = - crate::platform::run_exe_in_cur_session(f, vec!["--update"], false) - { - log::error!("Failed to run the update exe: {}", e); - } - } else if f.ends_with(".msi") { - if let Err(e) = crate::platform::update_me_msi(f, false) { - log::error!("Failed to run the update msi: {}", e); - } - } else { - // unreachable!() - } - #[cfg(target_os = "macos")] + + #[cfg(any(target_os = "windows", target_os = "macos"))] match crate::platform::update_to(f) { Ok(_) => { - log::info!("Update successfully!"); + log::info!("Update process is launched successfully!"); } Err(e) => { log::error!("Failed to update to new version, {}", e); + fs::remove_file(f).ok(); } } - fs::remove_file(f).ok(); } } } else if _key == "extract-update-dmg" { diff --git a/src/hbbs_http/downloader.rs b/src/hbbs_http/downloader.rs index 2afa2ba28..573e7e77c 100644 --- a/src/hbbs_http/downloader.rs +++ b/src/hbbs_http/downloader.rs @@ -53,8 +53,25 @@ pub fn download_file( auto_del_dur: Option, ) -> ResultType { let id = url.clone(); - if DOWNLOADERS.lock().unwrap().contains_key(&id) { - return Ok(id); + // First pass: if a non-error downloader exists for this URL, reuse it. + // If an errored downloader exists, remove it so this call can retry. + let mut stale_path = None; + { + let mut downloaders = DOWNLOADERS.lock().unwrap(); + if let Some(downloader) = downloaders.get(&id) { + if downloader.error.is_none() { + return Ok(id); + } + stale_path = downloader.path.clone(); + downloaders.remove(&id); + } + } + if let Some(p) = stale_path { + if p.exists() { + if let Err(e) = std::fs::remove_file(&p) { + log::warn!("Failed to remove stale download file {}: {}", p.display(), e); + } + } } if let Some(path) = path.as_ref() { @@ -75,8 +92,26 @@ pub fn download_file( tx_cancel: tx, finished: false, }; - let mut downloaders = DOWNLOADERS.lock().unwrap(); - downloaders.insert(id.clone(), downloader); + // Second pass (atomic with insert) to avoid race with another concurrent caller. + let mut stale_path_after_check = None; + { + let mut downloaders = DOWNLOADERS.lock().unwrap(); + if let Some(existing) = downloaders.get(&id) { + if existing.error.is_none() { + return Ok(id); + } + stale_path_after_check = existing.path.clone(); + downloaders.remove(&id); + } + downloaders.insert(id.clone(), downloader); + } + if let Some(p) = stale_path_after_check { + if p.exists() { + if let Err(e) = std::fs::remove_file(&p) { + log::warn!("Failed to remove stale download file {}: {}", p.display(), e); + } + } + } let id2 = id.clone(); std::thread::spawn( diff --git a/src/platform/windows.rs b/src/platform/windows.rs index a45220eb4..ee8aa7c6f 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -153,11 +153,7 @@ pub fn clip_cursor(rect: Option<(i32, i32, i32, i32)>) -> bool { }; if result == FALSE { let err = GetLastError(); - log::warn!( - "ClipCursor failed: rect={:?}, error_code={}", - rect, - err - ); + log::warn!("ClipCursor failed: rect={:?}, error_code={}", rect, err); return false; } true @@ -757,15 +753,37 @@ pub fn run_as_user(arg: Vec<&str>) -> ResultType> { run_exe_in_cur_session(std::env::current_exe()?.to_str().unwrap_or(""), arg, false) } +pub fn run_exe_direct( + exe: &str, + arg: Vec<&str>, + show: bool, +) -> ResultType> { + let mut cmd = std::process::Command::new(exe); + for a in arg { + cmd.arg(a); + } + if !show { + cmd.creation_flags(CREATE_NO_WINDOW); + } + match cmd.spawn() { + Ok(child) => Ok(Some(child)), + Err(e) => bail!("Failed to start process: {}", e), + } +} + pub fn run_exe_in_cur_session( exe: &str, arg: Vec<&str>, show: bool, ) -> ResultType> { - let Some(session_id) = get_current_process_session_id() else { - bail!("Failed to get current process session id"); - }; - run_exe_in_session(exe, arg, session_id, show) + if is_root() { + let Some(session_id) = get_current_process_session_id() else { + bail!("Failed to get current process session id"); + }; + run_exe_in_session(exe, arg, session_id, show) + } else { + run_exe_direct(exe, arg, show) + } } pub fn run_exe_in_session( @@ -1331,6 +1349,38 @@ pub fn copy_exe_cmd(src_exe: &str, exe: &str, path: &str) -> ResultType )) } +#[inline] +pub fn rename_exe_cmd(src_exe: &str, path: &str) -> ResultType { + let src_exe_filename = PathBuf::from(src_exe) + .file_name() + .ok_or(anyhow!("Can't get file name of {src_exe}"))? + .to_string_lossy() + .to_string(); + let app_name = crate::get_app_name().to_lowercase(); + if src_exe_filename.to_lowercase() == format!("{app_name}.exe") { + Ok("".to_owned()) + } else { + Ok(format!( + " + move /Y \"{path}\\{src_exe_filename}\" \"{path}\\{app_name}.exe\" + ", + )) + } +} + +#[inline] +pub fn remove_meta_toml_cmd(is_msi: bool, path: &str) -> String { + if is_msi && crate::is_custom_client() { + format!( + " + del /F /Q \"{path}\\meta.toml\" + ", + ) + } else { + "".to_owned() + } +} + fn get_after_install( exe: &str, reg_value_start_menu_shortcuts: Option, @@ -1417,7 +1467,11 @@ pub fn install_me(options: &str, path: String, silent: bool, debug: bool) -> Res } let app_name = crate::get_app_name(); + let current_exe = std::env::current_exe()?; + let tmp_path = std::env::temp_dir().to_string_lossy().to_string(); + let cur_exe = current_exe.to_str().unwrap_or("").to_owned(); + let shortcut_icon_location = get_shortcut_icon_location(&cur_exe); let mk_shortcut = write_cmds( format!( " @@ -1426,6 +1480,7 @@ sLinkFile = \"{tmp_path}\\{app_name}.lnk\" Set oLink = oWS.CreateShortcut(sLinkFile) oLink.TargetPath = \"{exe}\" + {shortcut_icon_location} oLink.Save " ), @@ -1482,8 +1537,13 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{start_menu}\\\" reg_value_printer = "1".to_owned(); } - let meta = std::fs::symlink_metadata(std::env::current_exe()?)?; - let size = meta.len() / 1024; + let meta = std::fs::symlink_metadata(¤t_exe)?; + let mut size = meta.len() / 1024; + if let Some(parent_dir) = current_exe.parent() { + if let Some(d) = parent_dir.to_str() { + size = get_directory_size_kb(d); + } + } // https://docs.microsoft.com/zh-cn/windows/win32/msi/uninstall-registry-key?redirectedfrom=MSDNa // https://www.windowscentral.com/how-edit-registry-using-command-prompt-windows-10 // https://www.tenforums.com/tutorials/70903-add-remove-allowed-apps-through-windows-firewall-windows-10-a.html @@ -1536,7 +1596,7 @@ chcp 65001 md \"{path}\" {copy_exe} reg add {subkey} /f -reg add {subkey} /f /v DisplayIcon /t REG_SZ /d \"{exe}\" +reg add {subkey} /f /v DisplayIcon /t REG_SZ /d \"{display_icon}\" reg add {subkey} /f /v DisplayName /t REG_SZ /d \"{app_name}\" reg add {subkey} /f /v DisplayVersion /t REG_SZ /d \"{version}\" reg add {subkey} /f /v Version /t REG_SZ /d \"{version}\" @@ -1560,6 +1620,7 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\" {install_remote_printer} {sleep} ", + display_icon = get_custom_icon(&cur_exe).unwrap_or(exe.to_string()), version = crate::VERSION.replace("-", "."), build_date = crate::BUILD_DATE, after_install = get_after_install( @@ -1795,6 +1856,163 @@ fn get_reg_of(subkey: &str, name: &str) -> String { "".to_owned() } +fn get_public_base_dir() -> PathBuf { + if let Ok(allusersprofile) = std::env::var("ALLUSERSPROFILE") { + let path = PathBuf::from(&allusersprofile); + if path.exists() { + return path; + } + } + if let Ok(public) = std::env::var("PUBLIC") { + let path = PathBuf::from(public).join("Documents"); + if path.exists() { + return path; + } + } + let program_data_dir = PathBuf::from("C:\\ProgramData"); + if program_data_dir.exists() { + return program_data_dir; + } + std::env::temp_dir() +} + +#[inline] +pub fn get_custom_client_staging_dir() -> PathBuf { + get_public_base_dir() + .join("RustDesk") + .join("RustDeskCustomClientStaging") +} + +/// Removes the custom client staging directory. +/// +/// Current behavior: intentionally a no-op (does not delete). +/// +/// Rationale +/// - The staging directory only contains a small `custom.txt`, leaving it is harmless. +/// - Deleting directories under a public location (e.g., C:\\ProgramData\\RustDesk) is +/// susceptible to TOCTOU attacks if an unprivileged user can replace the path with a +/// symlink/junction between checks and deletion. +/// +/// Future work: +/// - Use the files (if needed) in the installation directory instead of a public location. +/// This directory only contains a small `custom.txt` file. +/// - Pass the custom client name directly via command line +/// or environment variable during update installation. Then no staging directory is needed. +#[inline] +pub fn remove_custom_client_staging_dir(staging_dir: &Path) -> ResultType { + if !staging_dir.exists() { + return Ok(false); + } + + // First explicitly removes `custom.txt` to ensure stale config is never replayed, + // even if the subsequent directory removal fails. + // + // `std::fs::remove_file` on a symlink removes the symlink itself, not the target, + // so this is safe even in a TOCTOU race. + let custom_txt_path = staging_dir.join("custom.txt"); + if custom_txt_path.exists() { + allow_err!(std::fs::remove_file(&custom_txt_path)); + } + + // Intentionally not deleting. See the function docs for rationale. + log::debug!( + "Skip deleting staging directory {:?} (intentional to avoid TOCTOU)", + staging_dir + ); + Ok(false) +} + +// Prepare custom client update by copying staged custom.txt to current directory and loading it. +// Returns: +// 1. Ok(true) if preparation was successful or no staging directory exists. +// 2. Ok(false) if custom.txt file exists but has invalid contents or fails security checks +// (e.g., is a symlink or has invalid contents). +// 3. Err if any unexpected error occurs during file operations. +pub fn prepare_custom_client_update() -> ResultType { + let custom_client_staging_dir = get_custom_client_staging_dir(); + let current_exe = std::env::current_exe()?; + let current_exe_dir = current_exe + .parent() + .ok_or(anyhow!("Cannot get parent directory of current exe"))?; + + let staging_dir = custom_client_staging_dir.clone(); + let clear_staging_on_exit = crate::SimpleCallOnReturn { + b: true, + f: Box::new( + move || match remove_custom_client_staging_dir(&staging_dir) { + Ok(existed) => { + if existed { + log::info!("Custom client staging directory removed successfully."); + } + } + Err(e) => { + log::error!( + "Failed to remove custom client staging directory {:?}: {}", + staging_dir, + e + ); + } + }, + ), + }; + + if custom_client_staging_dir.exists() { + let custom_txt_path = custom_client_staging_dir.join("custom.txt"); + if !custom_txt_path.exists() { + return Ok(true); + } + + let metadata = std::fs::symlink_metadata(&custom_txt_path)?; + if metadata.is_symlink() { + log::error!( + "custom.txt is a symlink. Refusing to load custom client for security reasons." + ); + drop(clear_staging_on_exit); + return Ok(false); + } + if metadata.is_file() { + // Copy custom.txt to current directory + let local_custom_file_path = current_exe_dir.join("custom.txt"); + log::debug!( + "Copying staged custom file from {:?} to {:?}", + custom_txt_path, + local_custom_file_path + ); + + // No need to check symlink before copying. + // `load_custom_client()` will fail if the file is not valid. + fs::copy(&custom_txt_path, &local_custom_file_path)?; + log::info!("Staged custom client file copied to current directory."); + + // Load custom client + let is_custom_file_exists = + local_custom_file_path.exists() && local_custom_file_path.is_file(); + crate::load_custom_client(); + + // Remove the copied custom.txt file + allow_err!(fs::remove_file(&local_custom_file_path)); + + // Check if loaded successfully + if is_custom_file_exists && !crate::common::is_custom_client() { + // The custom.txt file existed, but its contents are invalid. + log::error!("Failed to load custom client from custom.txt."); + drop(clear_staging_on_exit); + // ERROR_INVALID_DATA + return Ok(false); + } + } else { + log::info!("No custom client files found in staging directory."); + } + } else { + log::info!( + "Custom client staging directory {:?} does not exist.", + custom_client_staging_dir + ); + } + + Ok(true) +} + pub fn get_license_from_exe_name() -> ResultType { let mut exe = std::env::current_exe()?.to_str().unwrap_or("").to_owned(); // if defined portable appname entry, replace original executable name with it. @@ -1903,12 +2121,48 @@ unsafe fn set_default_dll_directories() -> bool { true } +fn get_custom_icon(exe: &str) -> Option { + if crate::is_custom_client() { + if let Some(p) = PathBuf::from(exe).parent() { + let alter_icon_path = p.join("data\\flutter_assets\\assets\\icon.ico"); + if alter_icon_path.exists() { + // Verify that the icon is not a symlink for security + if let Ok(metadata) = std::fs::symlink_metadata(&alter_icon_path) { + if metadata.is_symlink() { + log::warn!( + "Custom icon at {:?} is a symlink, refusing to use it.", + alter_icon_path + ); + return None; + } + if metadata.is_file() { + return Some(alter_icon_path.to_string_lossy().to_string()); + } + } + } + } + } + None +} + +#[inline] +fn get_shortcut_icon_location(exe: &str) -> String { + if exe.is_empty() { + return "".to_owned(); + } + + get_custom_icon(exe) + .map(|p| format!("oLink.IconLocation = \"{}\"", p)) + .unwrap_or_default() +} + pub fn create_shortcut(id: &str) -> ResultType<()> { let exe = std::env::current_exe()?.to_str().unwrap_or("").to_owned(); // https://github.com/rustdesk/rustdesk/issues/13735 // Replace ':' with '_' for filename since ':' is not allowed in Windows filenames // https://github.com/rustdesk/hbb_common/blob/8b0e25867375ba9e6bff548acf44fe6d6ffa7c0e/src/config.rs#L1384 let filename = id.replace(':', "_"); + let shortcut_icon_location = get_shortcut_icon_location(&exe); let shortcut = write_cmds( format!( " @@ -1919,6 +2173,7 @@ sLinkFile = objFSO.BuildPath(strDesktop, \"{filename}.lnk\") Set oLink = oWS.CreateShortcut(sLinkFile) oLink.TargetPath = \"{exe}\" oLink.Arguments = \"--connect {id}\" + {shortcut_icon_location} oLink.Save " ), @@ -2724,6 +2979,44 @@ if exist \"{tray_shortcut}\" del /f /q \"{tray_shortcut}\" std::process::exit(0); } +/// Calculate the total size of a directory in KB +/// Does not follow symlinks to prevent directory traversal attacks. +fn get_directory_size_kb(path: &str) -> u64 { + let mut total_size = 0u64; + let mut stack = vec![PathBuf::from(path)]; + + while let Some(current_path) = stack.pop() { + let entries = match std::fs::read_dir(¤t_path) { + Ok(entries) => entries, + Err(_) => continue, + }; + + for entry in entries { + let entry = match entry { + Ok(entry) => entry, + Err(_) => continue, + }; + + let metadata = match std::fs::symlink_metadata(entry.path()) { + Ok(metadata) => metadata, + Err(_) => continue, + }; + + if metadata.is_symlink() { + continue; + } + + if metadata.is_dir() { + stack.push(entry.path()); + } else { + total_size = total_size.saturating_add(metadata.len()); + } + } + } + + total_size / 1024 +} + pub fn update_me(debug: bool) -> ResultType<()> { let app_name = crate::get_app_name(); let src_exe = std::env::current_exe()?.to_string_lossy().to_string(); @@ -2764,12 +3057,35 @@ pub fn update_me(debug: bool) -> ResultType<()> { if versions.len() > 2 { version_build = versions[2]; } - let meta = std::fs::symlink_metadata(std::env::current_exe()?)?; - let size = meta.len() / 1024; + let version = crate::VERSION.replace("-", "."); + let size = get_directory_size_kb(&path); + let build_date = crate::BUILD_DATE; + let display_icon = get_custom_icon(&exe).unwrap_or(exe.to_string()); - let reg_cmd = format!( - " -reg add {subkey} /f /v DisplayIcon /t REG_SZ /d \"{exe}\" + let is_msi = is_msi_installed().ok(); + + fn get_reg_cmd( + subkey: &str, + is_msi: Option, + display_icon: &str, + version: &str, + build_date: &str, + version_major: &str, + version_minor: &str, + version_build: &str, + size: u64, + ) -> String { + let reg_display_icon = if is_msi.unwrap_or(false) { + "".to_string() + } else { + format!( + "reg add {} /f /v DisplayIcon /t REG_SZ /d \"{}\"", + subkey, display_icon + ) + }; + format!( + " +{reg_display_icon} reg add {subkey} /f /v DisplayVersion /t REG_SZ /d \"{version}\" reg add {subkey} /f /v Version /t REG_SZ /d \"{version}\" reg add {subkey} /f /v BuildDate /t REG_SZ /d \"{build_date}\" @@ -2777,10 +3093,39 @@ reg add {subkey} /f /v VersionMajor /t REG_DWORD /d {version_major} reg add {subkey} /f /v VersionMinor /t REG_DWORD /d {version_minor} reg add {subkey} /f /v VersionBuild /t REG_DWORD /d {version_build} reg add {subkey} /f /v EstimatedSize /t REG_DWORD /d {size} - ", - version = crate::VERSION.replace("-", "."), - build_date = crate::BUILD_DATE, - ); + " + ) + } + + let reg_cmd = { + let reg_cmd_main = get_reg_cmd( + &subkey, + is_msi, + &display_icon, + &version, + &build_date, + &version_major, + &version_minor, + &version_build, + size, + ); + let reg_cmd_msi = if let Some(reg_msi_key) = get_reg_msi_key(&subkey, is_msi) { + get_reg_cmd( + ®_msi_key, + is_msi, + &display_icon, + &version, + &build_date, + &version_major, + &version_minor, + &version_build, + size, + ) + } else { + "".to_owned() + }; + format!("{}{}", reg_cmd_main, reg_cmd_msi) + }; let filter = format!(" /FI \"PID ne {}\"", get_current_pid()); let restore_service_cmd = if is_service_running { @@ -2820,6 +3165,8 @@ sc stop {app_name} taskkill /F /IM {app_name}.exe{filter} {reg_cmd} {copy_exe} +{rename_exe} +{remove_meta_toml} {restore_service_cmd} {uninstall_printer_cmd} {install_printer_cmd} @@ -2827,43 +3174,106 @@ taskkill /F /IM {app_name}.exe{filter} ", app_name = app_name, copy_exe = copy_exe_cmd(&src_exe, &exe, &path)?, + rename_exe = rename_exe_cmd(&src_exe, &path)?, + remove_meta_toml = remove_meta_toml_cmd(is_msi.unwrap_or(true), &path), sleep = if debug { "timeout 300" } else { "" }, ); + let _restore_session_guard = crate::common::SimpleCallOnReturn { + b: true, + f: Box::new(move || { + let is_root = is_root(); + if tray_sessions.is_empty() { + log::info!("No tray process found."); + } else { + log::info!( + "Try to restore the tray process..., sessions: {:?}", + &tray_sessions + ); + // When not running as root, only spawn once since run_exe_direct + // doesn't target specific sessions. + let mut spawned_non_root_tray = false; + for s in tray_sessions.clone().into_iter() { + if s != 0 { + // We need to check if is_root here because if `update_me()` is called from + // the main window running with administrator permission, + // `run_exe_in_session()` will fail with error 1314 ("A required privilege is + // not held by the client"). + // + // This issue primarily affects the MSI-installed version running in Administrator + // session during testing, but we check permissions here to be safe. + if is_root { + allow_err!(run_exe_in_session(&exe, vec!["--tray"], s, true)); + } else if !spawned_non_root_tray { + // Only spawn once for non-root since run_exe_direct doesn't take session parameter + allow_err!(run_exe_direct(&exe, vec!["--tray"], false)); + spawned_non_root_tray = true; + } + } + } + } + if main_window_sessions.is_empty() { + log::info!("No main window process found."); + } else { + log::info!("Try to restore the main window process..."); + std::thread::sleep(std::time::Duration::from_millis(2000)); + // When not running as root, only spawn once since run_exe_direct + // doesn't target specific sessions. + let mut spawned_non_root_main = false; + for s in main_window_sessions.clone().into_iter() { + if s != 0 { + if is_root { + allow_err!(run_exe_in_session(&exe, vec![], s, true)); + } else if !spawned_non_root_main { + // Only spawn once for non-root since run_exe_direct doesn't take session parameter + allow_err!(run_exe_direct(&exe, vec![], false)); + spawned_non_root_main = true; + } + } + } + } + std::thread::sleep(std::time::Duration::from_millis(300)); + }), + }; + run_cmds(cmds, debug, "update")?; std::thread::sleep(std::time::Duration::from_millis(2000)); - if tray_sessions.is_empty() { - log::info!("No tray process found."); - } else { - log::info!("Try to restore the tray process..."); - log::info!( - "Try to restore the tray process..., sessions: {:?}", - &tray_sessions - ); - for s in tray_sessions { - if s != 0 { - allow_err!(run_exe_in_session(&exe, vec!["--tray"], s, true)); - } - } - } - if main_window_sessions.is_empty() { - log::info!("No main window process found."); - } else { - log::info!("Try to restore the main window process..."); - std::thread::sleep(std::time::Duration::from_millis(2000)); - for s in main_window_sessions { - if s != 0 { - allow_err!(run_exe_in_session(&exe, vec![], s, true)); - } - } - } - std::thread::sleep(std::time::Duration::from_millis(300)); log::info!("Update completed."); Ok(()) } +fn get_reg_msi_key(subkey: &str, is_msi: Option) -> Option { + // Only proceed if it's a custom client and MSI is installed. + // `is_msi.unwrap_or(true)` is intentional: subsequent code validates the registry, + // hence no early return is required upon MSI detection failure. + if !(crate::common::is_custom_client() && is_msi.unwrap_or(true)) { + return None; + } + + // Get the uninstall string from registry + let uninstall_string = get_reg_of(subkey, "UninstallString"); + if uninstall_string.is_empty() { + return None; + } + + // Find the product code (GUID) in the uninstall string + // Handle both quoted and unquoted GUIDs: /X {GUID} or /X "{GUID}" + let start = uninstall_string.rfind('{')?; + let end = uninstall_string.rfind('}')?; + if start >= end { + return None; + } + let product_code = &uninstall_string[start..=end]; + + // Build the MSI registry key path + let pos = subkey.rfind('\\')?; + let reg_msi_key = format!("{}{}", &subkey[..=pos], product_code); + + Some(reg_msi_key) +} + // Double confirm the process name fn kill_process_by_pids(name: &str, pids: Vec) -> ResultType<()> { let name = name.to_lowercase(); @@ -2885,6 +3295,109 @@ fn kill_process_by_pids(name: &str, pids: Vec) -> ResultType<()> { Ok(()) } +pub fn handle_custom_client_staging_dir_before_update( + custom_client_staging_dir: &PathBuf, +) -> ResultType<()> { + let Some(current_exe_dir) = std::env::current_exe() + .ok() + .and_then(|p| p.parent().map(|p| p.to_path_buf())) + else { + bail!("Failed to get current exe directory"); + }; + + // Clean up existing staging directory + if custom_client_staging_dir.exists() { + log::debug!( + "Removing existing custom client staging directory: {:?}", + custom_client_staging_dir + ); + if let Err(e) = remove_custom_client_staging_dir(custom_client_staging_dir) { + bail!( + "Failed to remove existing custom client staging directory {:?}: {}", + custom_client_staging_dir, + e + ); + } + } + + let src_path = current_exe_dir.join("custom.txt"); + if src_path.exists() { + // Verify that custom.txt is not a symlink before copying + let metadata = match std::fs::symlink_metadata(&src_path) { + Ok(m) => m, + Err(e) => { + bail!( + "Failed to read metadata for custom.txt at {:?}: {}", + src_path, + e + ); + } + }; + + if metadata.is_symlink() { + allow_err!(remove_custom_client_staging_dir(&custom_client_staging_dir)); + bail!( + "custom.txt at {:?} is a symlink, refusing to stage for security reasons.", + src_path + ); + } + + if metadata.is_file() { + if !custom_client_staging_dir.exists() { + if let Err(e) = std::fs::create_dir_all(custom_client_staging_dir) { + bail!("Failed to create parent directory {:?} when staging custom client files: {}", custom_client_staging_dir, e); + } + } + let dst_path = custom_client_staging_dir.join("custom.txt"); + if let Err(e) = std::fs::copy(&src_path, &dst_path) { + allow_err!(remove_custom_client_staging_dir(&custom_client_staging_dir)); + bail!( + "Failed to copy custom txt from {:?} to {:?}: {}", + src_path, + dst_path, + e + ); + } + } else { + log::warn!( + "custom.txt at {:?} is not a regular file, skipping.", + src_path + ); + } + } else { + log::info!("No custom txt found to stage for update."); + } + + Ok(()) +} + +// Used for auto update and manual update in the main window. +pub fn update_to(file: &str) -> ResultType<()> { + if file.ends_with(".exe") { + let custom_client_staging_dir = get_custom_client_staging_dir(); + if crate::is_custom_client() { + handle_custom_client_staging_dir_before_update(&custom_client_staging_dir)?; + } else { + // Clean up any residual staging directory from previous custom client + allow_err!(remove_custom_client_staging_dir(&custom_client_staging_dir)); + } + if !run_uac(file, "--update")? { + bail!( + "Failed to run the update exe with UAC, error: {:?}", + std::io::Error::last_os_error() + ); + } + } else if file.ends_with(".msi") { + if let Err(e) = update_me_msi(file, false) { + bail!("Failed to run the update msi: {}", e); + } + } else { + // unreachable!() + bail!("Unsupported update file format: {}", file); + } + Ok(()) +} + // Don't launch tray app when running with `\qn`. // 1. Because `/qn` requires administrator permission and the tray app should be launched with user permission. // Or launching the main window from the tray app will cause the main window to be launched with administrator permission. @@ -2905,6 +3418,7 @@ pub fn update_me_msi(msi: &str, quiet: bool) -> ResultType<()> { } pub fn get_tray_shortcut(exe: &str, tmp_path: &str) -> ResultType { + let shortcut_icon_location = get_shortcut_icon_location(exe); Ok(write_cmds( format!( " @@ -2914,6 +3428,7 @@ sLinkFile = \"{tmp_path}\\{app_name} Tray.lnk\" Set oLink = oWS.CreateShortcut(sLinkFile) oLink.TargetPath = \"{exe}\" oLink.Arguments = \"--tray\" + {shortcut_icon_location} oLink.Save ", app_name = crate::get_app_name(), @@ -2976,6 +3491,44 @@ fn run_after_run_cmds(silent: bool) { std::thread::sleep(std::time::Duration::from_millis(300)); } +#[inline] +pub fn try_remove_temp_update_files() { + let temp_dir = std::env::temp_dir(); + let Ok(entries) = std::fs::read_dir(&temp_dir) else { + log::debug!("Failed to read temp directory: {:?}", temp_dir); + return; + }; + + let one_hour = std::time::Duration::from_secs(60 * 60); + for entry in entries { + if let Ok(entry) = entry { + let path = entry.path(); + if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) { + // Match files like rustdesk-*.msi or rustdesk-*.exe + if file_name.starts_with("rustdesk-") + && (file_name.ends_with(".msi") || file_name.ends_with(".exe")) + { + // Skip files modified within the last hour to avoid deleting files being downloaded + if let Ok(metadata) = std::fs::metadata(&path) { + if let Ok(modified) = metadata.modified() { + if let Ok(elapsed) = modified.elapsed() { + if elapsed < one_hour { + continue; + } + } + } + } + if let Err(e) = std::fs::remove_file(&path) { + log::debug!("Failed to remove temp update file {:?}: {}", path, e); + } else { + log::info!("Removed temp update file: {:?}", path); + } + } + } + } + } +} + #[inline] pub fn try_kill_broker() { allow_err!(std::process::Command::new("cmd") @@ -3151,7 +3704,8 @@ pub fn is_x64() -> bool { pub fn try_kill_rustdesk_main_window_process() -> ResultType<()> { // Kill rustdesk.exe without extra arg, should only be called by --server // We can find the exact process which occupies the ipc, see more from https://github.com/winsiderss/systeminformer - log::info!("try kill rustdesk main window process"); + let app_name = crate::get_app_name().to_lowercase(); + log::info!("try kill main window process"); use hbb_common::sysinfo::System; let mut sys = System::new(); sys.refresh_processes(); @@ -3160,7 +3714,6 @@ pub fn try_kill_rustdesk_main_window_process() -> ResultType<()> { .map(|x| x.user_id()) .unwrap_or_default(); let my_pid = std::process::id(); - let app_name = crate::get_app_name().to_lowercase(); if app_name.is_empty() { bail!("app name is empty"); } diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index b3ab6a523..3ef280a2a 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -66,7 +66,7 @@ impl RendezvousMediator { } crate::hbbs_http::sync::start(); #[cfg(target_os = "windows")] - if crate::platform::is_installed() && crate::is_server() && !crate::is_custom_client() { + if crate::platform::is_installed() && crate::is_server() { crate::updater::start_auto_update(); } check_zombie(); diff --git a/src/ui/index.tis b/src/ui/index.tis index d4934ba0b..edd69312e 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -824,7 +824,9 @@ class UpdateMe: Reactor.Component { return
    {translate('Status')}
    There is a newer version of {handler.get_app_name()} ({handler.get_new_version()}) available.
    -
    {translate('Click to ' + update_or_download)}
    + {is_custom_client + ?
    {translate('Enable \"Auto update\" or contact your administrator for the latest version.')}
    + :
    {translate('Click to ' + update_or_download)}
    }
    ; } diff --git a/src/updater.rs b/src/updater.rs index c1ff60b46..357f111a7 100644 --- a/src/updater.rs +++ b/src/updater.rs @@ -119,7 +119,7 @@ fn start_auto_update_check_(rx_msg: Receiver) { fn check_update(manually: bool) -> ResultType<()> { #[cfg(target_os = "windows")] - let is_msi = crate::platform::is_msi_installed()?; + let update_msi = crate::platform::is_msi_installed()? && !crate::is_custom_client(); if !(manually || config::Config::get_bool_option(config::keys::OPTION_ALLOW_AUTO_UPDATE)) { return Ok(()); } @@ -140,7 +140,7 @@ fn check_update(manually: bool) -> ResultType<()> { "{}/rustdesk-{}-x86_64.{}", download_url, version, - if is_msi { "msi" } else { "exe" } + if update_msi { "msi" } else { "exe" } ) } else { format!("{}/rustdesk-{}-x86-sciter.exe", download_url, version) @@ -190,21 +190,21 @@ fn check_update(manually: bool) -> ResultType<()> { // before the download, but not empty after the download. if has_no_active_conns() { #[cfg(target_os = "windows")] - update_new_version(is_msi, &version, &file_path); + update_new_version(update_msi, &version, &file_path); } } Ok(()) } #[cfg(target_os = "windows")] -fn update_new_version(is_msi: bool, version: &str, file_path: &PathBuf) { +fn update_new_version(update_msi: bool, version: &str, file_path: &PathBuf) { log::debug!( - "New version is downloaded, update begin, is msi: {is_msi}, version: {version}, file: {:?}", + "New version is downloaded, update begin, update msi: {update_msi}, version: {version}, file: {:?}", file_path.to_str() ); if let Some(p) = file_path.to_str() { if let Some(session_id) = crate::platform::get_current_process_session_id() { - if is_msi { + if update_msi { match crate::platform::update_me_msi(p, true) { Ok(_) => { log::debug!("New version \"{}\" updated.", version); @@ -215,21 +215,57 @@ fn update_new_version(is_msi: bool, version: &str, file_path: &PathBuf) { version, e ); + std::fs::remove_file(&file_path).ok(); } } } else { - match crate::platform::launch_privileged_process( + let custom_client_staging_dir = if crate::is_custom_client() { + let custom_client_staging_dir = + crate::platform::get_custom_client_staging_dir(); + if let Err(e) = crate::platform::handle_custom_client_staging_dir_before_update( + &custom_client_staging_dir, + ) { + log::error!( + "Failed to handle custom client staging dir before update: {}", + e + ); + std::fs::remove_file(&file_path).ok(); + return; + } + Some(custom_client_staging_dir) + } else { + // Clean up any residual staging directory from previous custom client + let staging_dir = crate::platform::get_custom_client_staging_dir(); + hbb_common::allow_err!(crate::platform::remove_custom_client_staging_dir( + &staging_dir + )); + None + }; + let update_launched = match crate::platform::launch_privileged_process( session_id, &format!("{} --update", p), ) { Ok(h) => { if h.is_null() { log::error!("Failed to update to the new version: {}", version); + false + } else { + log::debug!("New version \"{}\" is launched.", version); + true } } Err(e) => { log::error!("Failed to run the new version: {}", e); + false } + }; + if !update_launched { + if let Some(dir) = custom_client_staging_dir { + hbb_common::allow_err!(crate::platform::remove_custom_client_staging_dir( + &dir + )); + } + std::fs::remove_file(&file_path).ok(); } } } else { @@ -237,6 +273,7 @@ fn update_new_version(is_msi: bool, version: &str, file_path: &PathBuf) { "Failed to get the current process session id, Error {}", std::io::Error::last_os_error() ); + std::fs::remove_file(&file_path).ok(); } } else { // unreachable!() From bb3501a4f9cba0d634c7f9e5908fbc605b3e0370 Mon Sep 17 00:00:00 2001 From: Amirhosein Akhlaghpoor Date: Sat, 28 Feb 2026 02:56:25 +0000 Subject: [PATCH 156/277] ui: scale wheel lines on Windows/Linux to Mac (#14395) * input: accelerate wheel bursts on Windows->Mac - boost fast wheel bursts without affecting single-step scrolls\n- use dominant-axis smooth detection and velocity gate\n- reset wheel timestamp on enter/leave\n- enforce single-axis scrolling\n- extract/tune Sciter wheel accel thresholds Signed-off-by: Amirhossein Akhlaghpour * input: clarify wheel burst tuning - add comments on acceleration rules and units\n- apply burst accel on Windows/Linux to macOS\n- reset wheel timing on enter/leave Signed-off-by: Amirhossein Akhlaghpour * input: align wheel burst velocity thresholds - match Flutter velocity gate with Sciter Signed-off-by: Amirhossein Akhlaghpour * input: restore flutter wheel velocity threshold - keep burst threshold at 0.002 delta/us Signed-off-by: Amirhossein Akhlaghpour --------- Signed-off-by: Amirhossein Akhlaghpour --- flutter/lib/models/input_model.dart | 50 +++++++++++++++++++++++++---- src/ui/remote.tis | 40 ++++++++++++++++++----- 2 files changed, 76 insertions(+), 14 deletions(-) diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 134b21107..628b27fb2 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -365,6 +365,16 @@ class InputModel { final isPhysicalMouse = false.obs; int _lastButtons = 0; Offset lastMousePos = Offset.zero; + int _lastWheelTsUs = 0; + + // Wheel acceleration thresholds. + static const int _wheelAccelFastThresholdUs = 40000; // 40ms + static const int _wheelAccelMediumThresholdUs = 80000; // 80ms + static const double _wheelBurstVelocityThreshold = + 0.002; // delta units per microsecond + // Wheel burst acceleration (empirical tuning). + // Applies only to fast, non-smooth bursts to preserve single-step scrolling. + // Flutter uses microseconds for dt, so velocity is in delta/us. // Relative mouse mode (for games/3D apps). final relativeMouseMode = false.obs; @@ -964,6 +974,7 @@ class InputModel { toReleaseRawKeys.release(handleRawKeyEvent); _pointerMovedAfterEnter = false; _pointerInsideImage = enter; + _lastWheelTsUs = 0; // Fix status if (!enter) { @@ -1407,17 +1418,44 @@ class InputModel { if (isViewOnly) return; if (isViewCamera) return; if (e is PointerScrollEvent) { - var dx = e.scrollDelta.dx.toInt(); - var dy = e.scrollDelta.dy.toInt(); + final rawDx = e.scrollDelta.dx; + final rawDy = e.scrollDelta.dy; + final dominantDelta = rawDx.abs() > rawDy.abs() ? rawDx.abs() : rawDy.abs(); + final isSmooth = dominantDelta < 1; + final nowUs = DateTime.now().microsecondsSinceEpoch; + final dtUs = _lastWheelTsUs == 0 ? 0 : nowUs - _lastWheelTsUs; + _lastWheelTsUs = nowUs; + int accel = 1; + if (!isSmooth && + dtUs > 0 && + dtUs <= _wheelAccelMediumThresholdUs && + (isWindows || isLinux) && + peerPlatform == kPeerPlatformMacOS) { + final velocity = dominantDelta / dtUs; + if (velocity >= _wheelBurstVelocityThreshold) { + if (dtUs < _wheelAccelFastThresholdUs) { + accel = 3; + } else { + accel = 2; + } + } + } + var dx = rawDx.toInt(); + var dy = rawDy.toInt(); + if (rawDx.abs() > rawDy.abs()) { + dy = 0; + } else { + dx = 0; + } if (dx > 0) { - dx = -1; + dx = -accel; } else if (dx < 0) { - dx = 1; + dx = accel; } if (dy > 0) { - dy = -1; + dy = -accel; } else if (dy < 0) { - dy = 1; + dy = accel; } bind.sessionSendMouse( sessionId: sessionId, diff --git a/src/ui/remote.tis b/src/ui/remote.tis index 0dd574af7..7602432fe 100644 --- a/src/ui/remote.tis +++ b/src/ui/remote.tis @@ -142,6 +142,14 @@ function resetWheel() { } var INERTIA_ACCELERATION = 30; +var WHEEL_ACCEL_VELOCITY_THRESHOLD = 5000; +var WHEEL_ACCEL_DT_FAST = 0.04; +var WHEEL_ACCEL_DT_MEDIUM = 0.08; +var WHEEL_ACCEL_VALUE_FAST = 3; +var WHEEL_ACCEL_VALUE_MEDIUM = 2; +// Wheel burst acceleration (empirical tuning). +// Applies only on fast, non-smooth wheel bursts to keep single-step scroll unchanged. +// Sciter uses seconds for dt, so velocity is in delta/sec. // not good, precision not enough to simulate acceleration effect, // seems have to use pixel based rather line based delta @@ -237,12 +245,28 @@ function handler.onMouse(evt) // mouseWheelDistance = 8 * [currentUserDefs floatForKey:@"com.apple.scrollwheel.scaling"]; mask = 3; { - var (dx, dy) = evt.wheelDeltas; - if (dx > 0) dx = 1; - else if (dx < 0) dx = -1; - if (dy > 0) dy = 1; - else if (dy < 0) dy = -1; - if (Math.abs(dx) > Math.abs(dy)) { + var now = getTime(); + var dt = last_wheel_time > 0 ? (now - last_wheel_time) / 1000 : 0; + var (raw_dx, raw_dy) = evt.wheelDeltas; + var dx = 0; + var dy = 0; + var abs_dx = Math.abs(raw_dx); + var abs_dy = Math.abs(raw_dy); + var dominant = abs_dx > abs_dy ? abs_dx : abs_dy; + var is_smooth = dominant < 1; + var accel = 1; + if (!is_smooth && dt > 0 && (is_win || is_linux) && get_peer_platform() == "Mac OS") { + var velocity = dominant / dt; + if (velocity >= WHEEL_ACCEL_VELOCITY_THRESHOLD) { + if (dt < WHEEL_ACCEL_DT_FAST) accel = WHEEL_ACCEL_VALUE_FAST; + else if (dt < WHEEL_ACCEL_DT_MEDIUM) accel = WHEEL_ACCEL_VALUE_MEDIUM; + } + } + if (raw_dx > 0) dx = accel; + else if (raw_dx < 0) dx = -accel; + if (raw_dy > 0) dy = accel; + else if (raw_dy < 0) dy = -accel; + if (abs_dx > abs_dy) { dy = 0; } else { dx = 0; @@ -253,8 +277,6 @@ function handler.onMouse(evt) wheel_delta_y = acc_wheel_delta_y.toInteger(); acc_wheel_delta_x -= wheel_delta_x; acc_wheel_delta_y -= wheel_delta_y; - var now = getTime(); - var dt = last_wheel_time > 0 ? (now - last_wheel_time) / 1000 : 0; if (dt > 0) { var vx = dx / dt; var vy = dy / dt; @@ -297,11 +319,13 @@ function handler.onMouse(evt) entered = true; stdout.println("enter"); handler.enter(handler.get_keyboard_mode()); + last_wheel_time = 0; return keyboard_enabled; case Event.MOUSE_LEAVE: entered = false; stdout.println("leave"); handler.leave(handler.get_keyboard_mode()); + last_wheel_time = 0; if (is_left_down && get_peer_platform() == "Android") { is_left_down = false; handler.send_mouse((1 << 3) | 2, 0, 0, evt.altKey, From e4208aa9cfa57586225668d50b76cfe4d95b9fc0 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sat, 28 Feb 2026 16:33:54 +0800 Subject: [PATCH 157/277] fix(update): revert check (#14423) Signed-off-by: fufesou --- flutter/lib/desktop/pages/desktop_home_page.dart | 8 +++----- src/common.rs | 4 +--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index b9af2dc7b..339ecddb0 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -430,12 +430,10 @@ class _DesktopHomePageState extends State } Widget buildHelpCards(String updateUrl) { - final isWindowsInstalled = isWindows && bind.mainIsInstalled(); - if (updateUrl.isNotEmpty && + if (!bind.isCustomClient() && + updateUrl.isNotEmpty && !isCardClosed && - (isWindowsInstalled || - (!bind.isCustomClient() && - bind.mainUriPrefixSync().contains('rustdesk')))) { + bind.mainUriPrefixSync().contains('rustdesk')) { final isToUpdate = (isWindows || isMacOS) && bind.mainIsInstalled(); String btnText = isToUpdate ? 'Update' : 'Download'; GestureTapCallback onPressed = () async { diff --git a/src/common.rs b/src/common.rs index d2c252869..3e23770c6 100644 --- a/src/common.rs +++ b/src/common.rs @@ -940,9 +940,7 @@ pub fn is_modifier(evt: &KeyEvent) -> bool { } pub fn check_software_update() { - let is_windows_installed = cfg!(target_os = "windows") && is_installed(); - let should_check_update = is_windows_installed || !is_custom_client(); - if !should_check_update { + if is_custom_client() { return; } let opt = LocalConfig::get_option(keys::OPTION_ENABLE_CHECK_UPDATE); From 1833cb0655d002a3beea4f972b8770c424d81d44 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sat, 28 Feb 2026 18:17:26 +0800 Subject: [PATCH 158/277] fix(update): revert check (#14424) Signed-off-by: fufesou --- flutter/lib/common.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index ca52c61e0..ab1b0b3c5 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -3938,9 +3938,7 @@ void earlyAssert() { void checkUpdate() { if (!isWeb) { - final isWindowsInstalled = isWindows && bind.mainIsInstalled(); - final shouldCheckUpdate = isWindowsInstalled || !bind.isCustomClient(); - if (shouldCheckUpdate) { + if (!bind.isCustomClient()) { platformFFI.registerEventHandler( kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish, (Map evt) async { From cd7e3e45059d2ad061a075be21bb60dc469d5ae2 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sun, 1 Mar 2026 15:19:07 +0800 Subject: [PATCH 159/277] fix(update): macos, input password (#14430) Signed-off-by: fufesou --- src/platform/macos.rs | 46 +++++++++++---------- src/platform/privileges_scripts/update.scpt | 21 ++++++---- 2 files changed, 37 insertions(+), 30 deletions(-) diff --git a/src/platform/macos.rs b/src/platform/macos.rs index b923c6c17..b9db741e1 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -279,6 +279,9 @@ fn update_daemon_agent(agent_plist_file: String, update_source_dir: String, sync Err(e) => { log::error!("run osascript failed: {}", e); } + Ok(status) if !status.success() => { + log::warn!("run osascript failed with status: {}", status); + } _ => { let installed = std::path::Path::new(&agent_plist_file).exists(); log::info!("Agent file {} installed: {}", &agent_plist_file, installed); @@ -851,32 +854,33 @@ pub fn update_me() -> ResultType<()> { if is_installed_daemon && !is_service_stopped { let agent = format!("{}_server.plist", crate::get_full_name()); let agent_plist_file = format!("/Library/LaunchAgents/{}", agent); - std::process::Command::new("launchctl") - .args(&["unload", "-w", &agent_plist_file]) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .ok(); update_daemon_agent(agent_plist_file, app_dir, true); } else { // `kill -9` may not work without "administrator privileges" - let update_body = format!( - r#" -do shell script " -pgrep -x '{app_name}' | grep -v {pid} | xargs kill -9 && rm -rf '/Applications/{app_name}.app' && ditto '{app_dir}' '/Applications/{app_name}.app' && chown -R {user}:staff '/Applications/{app_name}.app' && xattr -r -d com.apple.quarantine '/Applications/{app_name}.app' -" with prompt "{app_name} wants to update itself" with administrator privileges - "#, - app_name = app_name, - pid = std::process::id(), - app_dir = app_dir, - user = get_active_username() - ); - match Command::new("osascript") + let update_body = r#" +on run {app_name, cur_pid, app_dir, user_name} + set app_bundle to "/Applications/" & app_name & ".app" + set app_bundle_q to quoted form of app_bundle + set app_dir_q to quoted form of app_dir + set user_name_q to quoted form of user_name + + set kill_others to "pids=$(pgrep -x '" & app_name & "' | grep -vx " & cur_pid & " || true); if [ -n \"$pids\" ]; then echo \"$pids\" | xargs kill -9 || true; fi;" + set copy_files to "rm -rf " & app_bundle_q & " && ditto " & app_dir_q & " " & app_bundle_q & " && chown -R " & user_name_q & ":staff " & app_bundle_q & " && (xattr -r -d com.apple.quarantine " & app_bundle_q & " || true);" + set sh to "set -e;" & kill_others & copy_files + + do shell script sh with prompt app_name & " wants to update itself" with administrator privileges +end run + "#; + let active_user = get_active_username(); + let status = Command::new("osascript") .arg("-e") .arg(update_body) - .status() - { + .arg(app_name.to_string()) + .arg(std::process::id().to_string()) + .arg(app_dir) + .arg(active_user) + .status(); + match status { Ok(status) if !status.success() => { log::error!("osascript execution failed with status: {}", status); } diff --git a/src/platform/privileges_scripts/update.scpt b/src/platform/privileges_scripts/update.scpt index dffb70bd7..88f4bdde5 100644 --- a/src/platform/privileges_scripts/update.scpt +++ b/src/platform/privileges_scripts/update.scpt @@ -1,18 +1,21 @@ on run {daemon_file, agent_file, user, cur_pid, source_dir} - set unload_service to "launchctl unload -w /Library/LaunchDaemons/com.carriez.RustDesk_service.plist || true;" + set agent_plist to "/Library/LaunchAgents/com.carriez.RustDesk_server.plist" + set daemon_plist to "/Library/LaunchDaemons/com.carriez.RustDesk_service.plist" + set app_bundle to "/Applications/RustDesk.app" - set kill_others to "pgrep -x 'RustDesk' | grep -v " & cur_pid & " | xargs kill -9;" + set resolve_uid to "uid=$(id -u " & quoted form of user & " 2>/dev/null || true);" + set unload_agent to "if [ -n \"$uid\" ]; then launchctl bootout gui/$uid " & quoted form of agent_plist & " 2>/dev/null || launchctl bootout user/$uid " & quoted form of agent_plist & " 2>/dev/null || launchctl unload -w " & quoted form of agent_plist & " || true; else launchctl unload -w " & quoted form of agent_plist & " || true; fi;" + set unload_service to "launchctl unload -w " & daemon_plist & " || true;" + set kill_others to "pids=$(pgrep -x 'RustDesk' | grep -vx " & cur_pid & " || true); if [ -n \"$pids\" ]; then echo \"$pids\" | xargs kill -9 || true; fi;" - set copy_files to "rm -rf /Applications/RustDesk.app && ditto " & source_dir & " /Applications/RustDesk.app && chown -R " & quoted form of user & ":staff /Applications/RustDesk.app && xattr -r -d com.apple.quarantine /Applications/RustDesk.app;" + set copy_files to "(rm -rf " & quoted form of app_bundle & " && ditto " & quoted form of source_dir & " " & quoted form of app_bundle & " && chown -R " & quoted form of user & ":staff " & quoted form of app_bundle & " && (xattr -r -d com.apple.quarantine " & quoted form of app_bundle & " || true)) || exit 1;" - set sh1 to "echo " & quoted form of daemon_file & " > /Library/LaunchDaemons/com.carriez.RustDesk_service.plist && chown root:wheel /Library/LaunchDaemons/com.carriez.RustDesk_service.plist;" + set write_daemon_plist to "echo " & quoted form of daemon_file & " > " & daemon_plist & " && chown root:wheel " & daemon_plist & ";" + set write_agent_plist to "echo " & quoted form of agent_file & " > " & agent_plist & " && chown root:wheel " & agent_plist & ";" + set load_service to "launchctl load -w " & daemon_plist & ";" - set sh2 to "echo " & quoted form of agent_file & " > /Library/LaunchAgents/com.carriez.RustDesk_server.plist && chown root:wheel /Library/LaunchAgents/com.carriez.RustDesk_server.plist;" - - set sh3 to "launchctl load -w /Library/LaunchDaemons/com.carriez.RustDesk_service.plist;" - - set sh to unload_service & kill_others & copy_files & sh1 & sh2 & sh3 + set sh to "set -e;" & resolve_uid & unload_agent & unload_service & kill_others & copy_files & write_daemon_plist & write_agent_plist & load_service do shell script sh with prompt "RustDesk wants to update itself" with administrator privileges end run From 9cb6f38aea4695dcb5b6d5a903f5d959b31d5df2 Mon Sep 17 00:00:00 2001 From: MichaIng Date: Sun, 1 Mar 2026 11:05:19 +0100 Subject: [PATCH 160/277] packaging: deb: remove obsolete Python version check (#14429) It was used to conditionally install a Python module in the past. But that is not the case anymore since https://github.com/rustdesk/rustdesk/commit/37dbfcc. Now the check is obsolete. Due to `set -e`, the check leads to a package configuration failure if Python is not installed, which however otherwise is not needed for RustDesk. The commit includes an indentation fix and trailing space removal. Signed-off-by: MichaIng --- res/DEBIAN/postinst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/res/DEBIAN/postinst b/res/DEBIAN/postinst index dad333ee5..57bb30d61 100755 --- a/res/DEBIAN/postinst +++ b/res/DEBIAN/postinst @@ -6,15 +6,13 @@ if [ "$1" = configure ]; then INITSYS=$(ls -al /proc/1/exe | awk -F' ' '{print $NF}' | awk -F'/' '{print $NF}') ln -f -s /usr/share/rustdesk/rustdesk /usr/bin/rustdesk - + if [ "systemd" == "$INITSYS" ]; then if [ -e /etc/systemd/system/rustdesk.service ]; then rm /etc/systemd/system/rustdesk.service /usr/lib/systemd/system/rustdesk.service /usr/lib/systemd/user/rustdesk.service >/dev/null 2>&1 fi - version=$(python3 -V 2>&1 | grep -Po '(?<=Python )(.+)') - parsedVersion=$(echo "${version//./}") - mkdir -p /usr/lib/systemd/system/ + mkdir -p /usr/lib/systemd/system/ cp /usr/share/rustdesk/files/systemd/rustdesk.service /usr/lib/systemd/system/rustdesk.service # try fix error in Ubuntu 18.04 # Failed to reload rustdesk.service: Unit rustdesk.service is not loaded properly: Exec format error. From 80a5865db3a49036d4d57b64455e8ff87cc39854 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sun, 1 Mar 2026 20:06:04 +0800 Subject: [PATCH 161/277] macOS update: restore LaunchAgent in GUI session and isolate temp update dir by euid (#14434) * fix(update): macos, load agent Signed-off-by: fufesou * fix(update): macos, isolate temp update dir by euid Signed-off-by: fufesou * refact(update): macos script Signed-off-by: fufesou --------- Signed-off-by: fufesou --- src/platform/macos.rs | 48 ++++++++++----------- src/platform/privileges_scripts/update.scpt | 6 ++- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/src/platform/macos.rs b/src/platform/macos.rs index b9db741e1..22a1085f6 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -42,9 +42,16 @@ static PRIVILEGES_SCRIPTS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/platform/privileges_scripts"); static mut LATEST_SEED: i32 = 0; -// Using a fixed temporary directory for updates is preferable to -// using one that includes the custom client name. -const UPDATE_TEMP_DIR: &str = "/tmp/.rustdeskupdate"; +#[inline] +fn get_update_temp_dir() -> PathBuf { + let euid = unsafe { hbb_common::libc::geteuid() }; + Path::new("/tmp").join(format!(".rustdeskupdate-{}", euid)) +} + +#[inline] +fn get_update_temp_dir_string() -> String { + get_update_temp_dir().to_string_lossy().into_owned() +} /// Global mutex to serialize CoreGraphics cursor operations. /// This prevents race conditions between cursor visibility (hide depth tracking) @@ -285,21 +292,6 @@ fn update_daemon_agent(agent_plist_file: String, update_source_dir: String, sync _ => { let installed = std::path::Path::new(&agent_plist_file).exists(); log::info!("Agent file {} installed: {}", &agent_plist_file, installed); - if installed { - // Unload first, or load may not work if already loaded. - // We hope that the load operation can immediately trigger a start. - std::process::Command::new("launchctl") - .args(&["unload", "-w", &agent_plist_file]) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .ok(); - let status = std::process::Command::new("launchctl") - .args(&["load", "-w", &agent_plist_file]) - .status(); - log::info!("launch server, status: {:?}", &status); - } } } }; @@ -418,7 +410,9 @@ pub fn set_cursor_pos(x: i32, y: i32) -> bool { let _guard = match CG_CURSOR_MUTEX.try_lock() { Ok(guard) => guard, Err(std::sync::TryLockError::WouldBlock) => { - log::error!("[BUG] set_cursor_pos: CG_CURSOR_MUTEX is already held - potential deadlock!"); + log::error!( + "[BUG] set_cursor_pos: CG_CURSOR_MUTEX is already held - potential deadlock!" + ); debug_assert!(false, "Re-entrant call to set_cursor_pos detected"); return false; } @@ -825,7 +819,8 @@ pub fn quit_gui() { #[inline] pub fn try_remove_temp_update_dir(dir: Option<&str>) { - let target_path = Path::new(dir.unwrap_or(UPDATE_TEMP_DIR)); + let target_path_buf = dir.map(PathBuf::from).unwrap_or_else(get_update_temp_dir); + let target_path = target_path_buf.as_path(); if target_path.exists() { std::fs::remove_dir_all(target_path).ok(); } @@ -901,25 +896,28 @@ end run } pub fn update_from_dmg(dmg_path: &str) -> ResultType<()> { + let update_temp_dir = get_update_temp_dir_string(); println!("Starting update from DMG: {}", dmg_path); - extract_dmg(dmg_path, UPDATE_TEMP_DIR)?; + extract_dmg(dmg_path, &update_temp_dir)?; println!("DMG extracted"); - update_extracted(UPDATE_TEMP_DIR)?; + update_extracted(&update_temp_dir)?; println!("Update process started"); Ok(()) } pub fn update_to(_file: &str) -> ResultType<()> { - update_extracted(UPDATE_TEMP_DIR)?; + let update_temp_dir = get_update_temp_dir_string(); + update_extracted(&update_temp_dir)?; Ok(()) } pub fn extract_update_dmg(file: &str) { + let update_temp_dir = get_update_temp_dir_string(); let mut evt: HashMap<&str, String> = HashMap::from([("name", "extract-update-dmg".to_string())]); - match extract_dmg(file, UPDATE_TEMP_DIR) { + match extract_dmg(file, &update_temp_dir) { Ok(_) => { - log::info!("Extracted dmg file to {}", UPDATE_TEMP_DIR); + log::info!("Extracted dmg file to {}", update_temp_dir); } Err(e) => { evt.insert("err", e.to_string()); diff --git a/src/platform/privileges_scripts/update.scpt b/src/platform/privileges_scripts/update.scpt index 88f4bdde5..07dadb7c6 100644 --- a/src/platform/privileges_scripts/update.scpt +++ b/src/platform/privileges_scripts/update.scpt @@ -14,8 +14,12 @@ on run {daemon_file, agent_file, user, cur_pid, source_dir} set write_daemon_plist to "echo " & quoted form of daemon_file & " > " & daemon_plist & " && chown root:wheel " & daemon_plist & ";" set write_agent_plist to "echo " & quoted form of agent_file & " > " & agent_plist & " && chown root:wheel " & agent_plist & ";" set load_service to "launchctl load -w " & daemon_plist & ";" + set agent_label_cmd to "agent_label=$(basename " & quoted form of agent_plist & " .plist);" + set bootstrap_agent to "if [ -n \"$uid\" ]; then launchctl bootstrap gui/$uid " & quoted form of agent_plist & " 2>/dev/null || launchctl bootstrap user/$uid " & quoted form of agent_plist & " 2>/dev/null || launchctl load -w " & quoted form of agent_plist & " || true; else launchctl load -w " & quoted form of agent_plist & " || true; fi;" + set kickstart_agent to "if [ -n \"$uid\" ]; then launchctl kickstart -k gui/$uid/$agent_label 2>/dev/null || launchctl kickstart -k user/$uid/$agent_label 2>/dev/null || true; fi;" + set load_agent to agent_label_cmd & bootstrap_agent & kickstart_agent - set sh to "set -e;" & resolve_uid & unload_agent & unload_service & kill_others & copy_files & write_daemon_plist & write_agent_plist & load_service + set sh to "set -e;" & resolve_uid & unload_agent & unload_service & kill_others & copy_files & write_daemon_plist & write_agent_plist & load_service & load_agent do shell script sh with prompt "RustDesk wants to update itself" with administrator privileges end run From 6ba23683d5d6b5412b1cf819a95fd471801a62d5 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 2 Mar 2026 12:06:20 +0800 Subject: [PATCH 162/277] avatar in libs/hbb_comon --- libs/hbb_common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/hbb_common b/libs/hbb_common index 5e07db744..ae3726dd5 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 5e07db7444284006c008b5b1204f0968bc47b1a9 +Subproject commit ae3726dd5f505b87b8be66f2b2cf4e902a2dcde4 From 157dbdc543470292d87d18b93efc952ef380e66c Mon Sep 17 00:00:00 2001 From: rustdesk Date: Mon, 2 Mar 2026 12:14:26 +0800 Subject: [PATCH 163/277] fix avatar in hbb_common --- libs/hbb_common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/hbb_common b/libs/hbb_common index ae3726dd5..48c37de3e 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit ae3726dd5f505b87b8be66f2b2cf4e902a2dcde4 +Subproject commit 48c37de3e6c4e399af6f51ca20e8e3e1fd037976 From 732b2508159d66c21aa1250ef92aef4095f170e6 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 2 Mar 2026 19:07:09 +0800 Subject: [PATCH 164/277] fix(keyboard): legacy mode (#14435) * fix(keyboard): legacy mode Signed-off-by: fufesou * Simple refactor Signed-off-by: fufesou * fix(keyboard): legacy mode, chr to seq Signed-off-by: fufesou * fix(keyboard): legacy mode, early return if (!hotkey)&down Signed-off-by: fufesou * fix(keyboard): legacy mode, pair down/up Signed-off-by: fufesou --------- Signed-off-by: fufesou --- libs/enigo/src/win/win_impl.rs | 15 ++++++++++++-- src/server/input_service.rs | 37 +++++++++++++++++++++++++++++++--- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/libs/enigo/src/win/win_impl.rs b/libs/enigo/src/win/win_impl.rs index 882dba126..a6b465ea1 100644 --- a/libs/enigo/src/win/win_impl.rs +++ b/libs/enigo/src/win/win_impl.rs @@ -269,7 +269,7 @@ impl KeyboardControllable for Enigo { for pos in 0..mod_len { let rpos = mod_len - 1 - pos; if flag & (0x0001 << rpos) != 0 { - self.key_up(modifiers[pos]); + self.key_up(modifiers[rpos]); } } @@ -298,7 +298,18 @@ impl KeyboardControllable for Enigo { } fn key_up(&mut self, key: Key) { - keybd_event(KEYEVENTF_KEYUP, self.key_to_keycode(key), 0); + match key { + Key::Layout(c) => { + let code = self.get_layoutdependent_keycode(c); + if code as u16 != 0xFFFF { + let vk = code & 0x00FF; + keybd_event(KEYEVENTF_KEYUP, vk, 0); + } + } + _ => { + keybd_event(KEYEVENTF_KEYUP, self.key_to_keycode(key), 0); + } + } } fn get_key_state(&mut self, key: Key) -> bool { diff --git a/src/server/input_service.rs b/src/server/input_service.rs index fb8441dde..97dc78755 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -809,7 +809,7 @@ fn record_key_is_control_key(record_key: u64) -> bool { #[inline] fn record_key_is_chr(record_key: u64) -> bool { - record_key < KEY_CHAR_START + record_key >= KEY_CHAR_START } #[inline] @@ -1513,6 +1513,27 @@ fn get_control_key_value(key_event: &KeyEvent) -> i32 { } } +#[inline] +fn has_hotkey_modifiers(key_event: &KeyEvent) -> bool { + key_event.modifiers.iter().any(|ck| { + let v = ck.value(); + v == ControlKey::Control.value() + || v == ControlKey::RControl.value() + || v == ControlKey::Meta.value() + || v == ControlKey::RWin.value() + || { + #[cfg(any(target_os = "windows", target_os = "linux"))] + { + v == ControlKey::Alt.value() || v == ControlKey::RAlt.value() + } + #[cfg(target_os = "macos")] + { + false + } + } + }) +} + fn release_unpressed_modifiers(en: &mut Enigo, key_event: &KeyEvent) { let ck_value = get_control_key_value(key_event); fix_modifiers(&key_event.modifiers[..], en, ck_value); @@ -1572,7 +1593,7 @@ fn need_to_uppercase(en: &mut Enigo) -> bool { get_modifier_state(Key::Shift, en) || get_modifier_state(Key::CapsLock, en) } -fn process_chr(en: &mut Enigo, chr: u32, down: bool) { +fn process_chr(en: &mut Enigo, chr: u32, down: bool, _hotkey: bool) { // On Wayland with uinput mode, use clipboard for character input #[cfg(target_os = "linux")] if !crate::platform::linux::is_x11() && wayland_use_uinput() { @@ -1587,6 +1608,16 @@ fn process_chr(en: &mut Enigo, chr: u32, down: bool) { } } + #[cfg(any(target_os = "macos", target_os = "windows"))] + if !_hotkey { + if down { + if let Ok(chr) = char::try_from(chr) { + en.key_sequence(&chr.to_string()); + } + } + return; + } + let key = char_value_to_key(chr); if down { @@ -1856,7 +1887,7 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { let record_key = chr as u64 + KEY_CHAR_START; record_pressed_key(KeysDown::EnigoKey(record_key), down); - process_chr(&mut en, chr, down) + process_chr(&mut en, chr, down, has_hotkey_modifiers(evt)) } Some(key_event::Union::Unicode(chr)) => { // Same as Chr: release Shift for Unicode input From 41ab5bbdd8d6c56f99f59176ed3bc100762522f1 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Tue, 3 Mar 2026 10:47:32 +0800 Subject: [PATCH 165/277] fix(update): macos, test before update (#14446) Signed-off-by: fufesou --- src/platform/macos.rs | 3 ++- src/platform/privileges_scripts/update.scpt | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 22a1085f6..2e68cf5d8 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -859,9 +859,10 @@ on run {app_name, cur_pid, app_dir, user_name} set app_dir_q to quoted form of app_dir set user_name_q to quoted form of user_name + set check_source to "test -d " & app_dir_q & " || exit 1;" set kill_others to "pids=$(pgrep -x '" & app_name & "' | grep -vx " & cur_pid & " || true); if [ -n \"$pids\" ]; then echo \"$pids\" | xargs kill -9 || true; fi;" set copy_files to "rm -rf " & app_bundle_q & " && ditto " & app_dir_q & " " & app_bundle_q & " && chown -R " & user_name_q & ":staff " & app_bundle_q & " && (xattr -r -d com.apple.quarantine " & app_bundle_q & " || true);" - set sh to "set -e;" & kill_others & copy_files + set sh to "set -e;" & check_source & kill_others & copy_files do shell script sh with prompt app_name & " wants to update itself" with administrator privileges end run diff --git a/src/platform/privileges_scripts/update.scpt b/src/platform/privileges_scripts/update.scpt index 07dadb7c6..0484c257a 100644 --- a/src/platform/privileges_scripts/update.scpt +++ b/src/platform/privileges_scripts/update.scpt @@ -4,6 +4,7 @@ on run {daemon_file, agent_file, user, cur_pid, source_dir} set daemon_plist to "/Library/LaunchDaemons/com.carriez.RustDesk_service.plist" set app_bundle to "/Applications/RustDesk.app" + set check_source to "test -d " & quoted form of source_dir & " || exit 1;" set resolve_uid to "uid=$(id -u " & quoted form of user & " 2>/dev/null || true);" set unload_agent to "if [ -n \"$uid\" ]; then launchctl bootout gui/$uid " & quoted form of agent_plist & " 2>/dev/null || launchctl bootout user/$uid " & quoted form of agent_plist & " 2>/dev/null || launchctl unload -w " & quoted form of agent_plist & " || true; else launchctl unload -w " & quoted form of agent_plist & " || true; fi;" set unload_service to "launchctl unload -w " & daemon_plist & " || true;" @@ -19,7 +20,7 @@ on run {daemon_file, agent_file, user, cur_pid, source_dir} set kickstart_agent to "if [ -n \"$uid\" ]; then launchctl kickstart -k gui/$uid/$agent_label 2>/dev/null || launchctl kickstart -k user/$uid/$agent_label 2>/dev/null || true; fi;" set load_agent to agent_label_cmd & bootstrap_agent & kickstart_agent - set sh to "set -e;" & resolve_uid & unload_agent & unload_service & kill_others & copy_files & write_daemon_plist & write_agent_plist & load_service & load_agent + set sh to "set -e;" & check_source & resolve_uid & unload_agent & unload_service & kill_others & copy_files & write_daemon_plist & write_agent_plist & load_service & load_agent do shell script sh with prompt "RustDesk wants to update itself" with administrator privileges end run From 52b66e71d1a11bde30ab3347f210b8f81be7b85c Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:48:42 +0800 Subject: [PATCH 166/277] Move port mapping afterwards (#14448) * move port mapping after auth in port forwarding * fix(port-forward): try connect after 2fa Signed-off-by: fufesou * fix(security): gate port-forward connect on full auth and clarify login flow semantics Signed-off-by: fufesou * refact(port-forward): comments and logs Signed-off-by: fufesou --------- Signed-off-by: fufesou Co-authored-by: fufesou --- src/server/connection.rs | 130 ++++++++++++++++++++++++++------------- 1 file changed, 87 insertions(+), 43 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index 1259054cd..033aac0ce 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -560,7 +560,9 @@ impl Connection { match data { ipc::Data::Authorize => { conn.require_2fa.take(); - conn.send_logon_response().await; + if !conn.send_logon_response_and_keep_alive().await { + break; + } if conn.port_forward_socket.is_some() { break; } @@ -1338,9 +1340,66 @@ impl Connection { crate::post_request(url, v.to_string(), "").await } - async fn send_logon_response(&mut self) { + fn normalize_port_forward_target(pf: &mut PortForward) -> (String, bool) { + let mut is_rdp = false; + if pf.host == "RDP" && pf.port == 0 { + pf.host = "localhost".to_owned(); + pf.port = 3389; + is_rdp = true; + } + if pf.host.is_empty() { + pf.host = "localhost".to_owned(); + } + (format!("{}:{}", pf.host, pf.port), is_rdp) + } + + async fn connect_port_forward_if_needed(&mut self) -> bool { + if self.port_forward_socket.is_some() { + return true; + } + let Some(login_request::Union::PortForward(pf)) = self.lr.union.as_ref() else { + return true; + }; + let mut pf = pf.clone(); + let (mut addr, is_rdp) = Self::normalize_port_forward_target(&mut pf); + self.port_forward_address = addr.clone(); + match timeout(3000, TcpStream::connect(&addr)).await { + Ok(Ok(sock)) => { + self.port_forward_socket = Some(Framed::new(sock, BytesCodec::new())); + true + } + Ok(Err(e)) => { + log::warn!("Port forward connect failed for {}: {}", addr, e); + if is_rdp { + addr = "RDP".to_owned(); + } + self.send_login_error(format!( + "Failed to access remote {}. Please make sure it is reachable/open.", + addr + )) + .await; + false + } + Err(e) => { + log::warn!("Port forward connect timed out for {}: {}", addr, e); + if is_rdp { + addr = "RDP".to_owned(); + } + self.send_login_error(format!( + "Failed to access remote {}. Please make sure it is reachable/open.", + addr + )) + .await; + false + } + } + } + + // Returns whether this connection should be kept alive. + // `true` does not necessarily mean authorization succeeded (e.g. REQUIRE_2FA case). + async fn send_logon_response_and_keep_alive(&mut self) -> bool { if self.authorized { - return; + return true; } if self.require_2fa.is_some() && !self.is_recent_session(true) && !self.from_switch { self.require_2fa.as_ref().map(|totp| { @@ -1371,7 +1430,11 @@ impl Connection { } }); self.send_login_error(crate::client::REQUIRE_2FA).await; - return; + // Keep the connection alive so the client can continue with 2FA. + return true; + } + if !self.connect_port_forward_if_needed().await { + return false; } self.authorized = true; let (conn_type, auth_conn_type) = if self.file_transfer.is_some() { @@ -1494,7 +1557,7 @@ impl Connection { res.set_peer_info(pi); msg_out.set_login_response(res); self.send(msg_out).await; - return; + return true; } #[cfg(target_os = "linux")] if self.is_remote() { @@ -1517,7 +1580,7 @@ impl Connection { let mut msg_out = Message::new(); msg_out.set_login_response(res); self.send(msg_out).await; - return; + return true; } } #[allow(unused_mut)] @@ -1671,6 +1734,7 @@ impl Connection { self.try_sub_monitor_services(); } } + true } fn try_sub_camera_displays(&mut self) { @@ -2178,33 +2242,8 @@ impl Connection { sleep(1.).await; return false; } - let mut is_rdp = false; - if pf.host == "RDP" && pf.port == 0 { - pf.host = "localhost".to_owned(); - pf.port = 3389; - is_rdp = true; - } - if pf.host.is_empty() { - pf.host = "localhost".to_owned(); - } - let mut addr = format!("{}:{}", pf.host, pf.port); - self.port_forward_address = addr.clone(); - match timeout(3000, TcpStream::connect(&addr)).await { - Ok(Ok(sock)) => { - self.port_forward_socket = Some(Framed::new(sock, BytesCodec::new())); - } - _ => { - if is_rdp { - addr = "RDP".to_owned(); - } - self.send_login_error(format!( - "Failed to access remote {}, please make sure if it is open", - addr - )) - .await; - return false; - } - } + let (addr, _is_rdp) = Self::normalize_port_forward_target(&mut pf); + self.port_forward_address = addr; } _ => { if !self.check_privacy_mode_on().await { @@ -2235,9 +2274,7 @@ impl Connection { // `is_logon_ui()` is a fallback for logon UI detection on Windows. #[cfg(target_os = "windows")] let is_logon = || { - crate::platform::is_prelogin() - || crate::platform::is_locked() - || { + crate::platform::is_prelogin() || crate::platform::is_locked() || { match crate::platform::is_logon_ui() { Ok(result) => result, Err(e) => { @@ -2276,7 +2313,9 @@ impl Connection { if err_msg.is_empty() { #[cfg(target_os = "linux")] self.linux_headless_handle.wait_desktop_cm_ready().await; - self.send_logon_response().await; + if !self.send_logon_response_and_keep_alive().await { + return false; + } self.try_start_cm(lr.my_id.clone(), lr.my_name.clone(), self.authorized); } else { self.send_login_error(err_msg).await; @@ -2312,7 +2351,9 @@ impl Connection { if err_msg.is_empty() { #[cfg(target_os = "linux")] self.linux_headless_handle.wait_desktop_cm_ready().await; - self.send_logon_response().await; + if !self.send_logon_response_and_keep_alive().await { + return false; + } self.try_start_cm(lr.my_id, lr.my_name, self.authorized); } else { self.send_login_error(err_msg).await; @@ -2330,7 +2371,9 @@ impl Connection { self.update_failure(failure, true, 1); self.require_2fa.take(); raii::AuthedConnID::set_session_2fa(self.session_key()); - self.send_logon_response().await; + if !self.send_logon_response_and_keep_alive().await { + return false; + } self.try_start_cm( self.lr.my_id.to_owned(), self.lr.my_name.to_owned(), @@ -2381,7 +2424,9 @@ impl Connection { if let Some((_instant, uuid_old)) = uuid_old { if uuid == uuid_old { self.from_switch = true; - self.send_logon_response().await; + if !self.send_logon_response_and_keep_alive().await { + return false; + } self.try_start_cm( lr.my_id.clone(), lr.my_name.clone(), @@ -5347,9 +5392,8 @@ mod raii { } pub fn check_wake_lock_on_setting_changed() { - let current = config::Config::get_bool_option( - keys::OPTION_KEEP_AWAKE_DURING_INCOMING_SESSIONS, - ); + let current = + config::Config::get_bool_option(keys::OPTION_KEEP_AWAKE_DURING_INCOMING_SESSIONS); let cached = *WAKELOCK_KEEP_AWAKE_OPTION.lock().unwrap(); if cached != Some(current) { Self::check_wake_lock(); From ab64a32f301fda0b010eb9d029531b455b0faf80 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Wed, 4 Mar 2026 21:43:19 +0800 Subject: [PATCH 167/277] avatar (#14440) * avatar * refactor avatar display: unify rendering and resolve at use time - Extract buildAvatarWidget() in common.dart to share avatar rendering logic across desktop settings, desktop CM and mobile CM - Add resolve_avatar_url() in Rust, exposed via FFI (SyncReturn), to resolve relative avatar paths (e.g. "/avatar/xxx") to absolute URLs - Store avatar as-is in local config, only resolve when displaying (settings page) or sending (LoginRequest) - Resolve avatar in LoginRequest before sending to remote peer - Add error handling for network image load failures - Guard against empty client.name[0] crash - Show avatar in mobile settings page account tile Signed-off-by: 21pages * web: implement mainResolveAvatarUrl via js getByName Signed-off-by: 21pages * increase ipc Data enum size limit to 120 bytes Signed-off-by: 21pages --------- Signed-off-by: 21pages Co-authored-by: 21pages --- flutter/lib/common.dart | 40 +++++++++++ flutter/lib/common/hbbs/hbbs.dart | 3 + .../desktop/pages/desktop_setting_page.dart | 71 ++++++++++++++----- flutter/lib/desktop/pages/server_page.dart | 47 +++++++----- flutter/lib/mobile/pages/server_page.dart | 22 ++++-- flutter/lib/mobile/pages/settings_page.dart | 10 ++- flutter/lib/models/server_model.dart | 3 + flutter/lib/models/user_model.dart | 5 ++ flutter/lib/web/bridge.dart | 4 ++ src/client.rs | 17 ++++- src/flutter_ffi.rs | 4 ++ src/hbbs_http/account.rs | 4 ++ src/ipc.rs | 3 +- src/server/connection.rs | 1 + src/ui/cm.css | 5 ++ src/ui/cm.rs | 1 + src/ui/cm.tis | 7 +- src/ui/index.tis | 3 + src/ui_cm_interface.rs | 9 ++- src/ui_interface.rs | 15 +++- 20 files changed, 225 insertions(+), 49 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index ab1b0b3c5..af87f980f 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -4118,3 +4118,43 @@ String mouseButtonsToPeer(int buttons) { return ''; } } + +/// Build an avatar widget from an avatar URL or data URI string. +/// Returns [fallback] if avatar is empty or cannot be decoded. +/// [borderRadius] defaults to [size]/2 (circle). +Widget? buildAvatarWidget({ + required String avatar, + required double size, + double? borderRadius, + Widget? fallback, +}) { + final trimmed = avatar.trim(); + if (trimmed.isEmpty) return fallback; + + ImageProvider? imageProvider; + if (trimmed.startsWith('data:image/')) { + final comma = trimmed.indexOf(','); + if (comma > 0) { + try { + imageProvider = MemoryImage(base64Decode(trimmed.substring(comma + 1))); + } catch (_) {} + } + } else if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) { + imageProvider = NetworkImage(trimmed); + } + + if (imageProvider == null) return fallback; + + final radius = borderRadius ?? size / 2; + return ClipRRect( + borderRadius: BorderRadius.circular(radius), + child: Image( + image: imageProvider, + width: size, + height: size, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => + fallback ?? SizedBox.shrink(), + ), + ); +} diff --git a/flutter/lib/common/hbbs/hbbs.dart b/flutter/lib/common/hbbs/hbbs.dart index f3b210184..0c729e4df 100644 --- a/flutter/lib/common/hbbs/hbbs.dart +++ b/flutter/lib/common/hbbs/hbbs.dart @@ -26,6 +26,7 @@ enum UserStatus { kDisabled, kNormal, kUnverified } class UserPayload { String name = ''; String displayName = ''; + String avatar = ''; String email = ''; String note = ''; String? verifier; @@ -35,6 +36,7 @@ class UserPayload { UserPayload.fromJson(Map json) : name = json['name'] ?? '', displayName = json['display_name'] ?? '', + avatar = json['avatar'] ?? '', email = json['email'] ?? '', note = json['note'] ?? '', verifier = json['verifier'], @@ -49,6 +51,7 @@ class UserPayload { final Map map = { 'name': name, 'display_name': displayName, + 'avatar': avatar, 'status': status == UserStatus.kDisabled ? 0 : status == UserStatus.kUnverified diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index d8239adea..bde40cf19 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -2026,28 +2026,65 @@ class _AccountState extends State<_Account> { } Widget useInfo() { - text(String key, String value) { - return Align( - alignment: Alignment.centerLeft, - child: SelectionArea(child: Text('${translate(key)}: $value')) - .marginSymmetric(vertical: 4), - ); - } - return Obx(() => Offstage( offstage: gFFI.userModel.userName.value.isEmpty, - child: Column( - children: [ - if (gFFI.userModel.displayName.value.trim().isNotEmpty && - gFFI.userModel.displayName.value.trim() != - gFFI.userModel.userName.value.trim()) - text('Display Name', gFFI.userModel.displayName.value.trim()), - text('Username', gFFI.userModel.userName.value), - // text('Group', gFFI.groupModel.groupName.value), - ], + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(10), + ), + child: Builder(builder: (context) { + final avatarWidget = _buildUserAvatar(); + return Row( + children: [ + if (avatarWidget != null) avatarWidget, + if (avatarWidget != null) const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + gFFI.userModel.displayNameOrUserName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + SelectionArea( + child: Text( + '@${gFFI.userModel.userName.value}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 13, + color: + Theme.of(context).textTheme.bodySmall?.color, + ), + ), + ), + ], + ), + ), + ], + ); + }), ), )).marginOnly(left: 18, top: 16); } + + Widget? _buildUserAvatar() { + // Resolve relative avatar path at display time + final avatar = + bind.mainResolveAvatarUrl(avatar: gFFI.userModel.avatar.value); + return buildAvatarWidget( + avatar: avatar, + size: 44, + ); + } } class _Checkbox extends StatefulWidget { diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 4ee29756f..ea37c95e4 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -462,23 +462,7 @@ class _CmHeaderState extends State<_CmHeader> child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - width: 70, - height: 70, - alignment: Alignment.center, - decoration: BoxDecoration( - color: str2color(client.name), - borderRadius: BorderRadius.circular(15.0), - ), - child: Text( - client.name[0], - style: TextStyle( - fontWeight: FontWeight.bold, - color: Colors.white, - fontSize: 55, - ), - ), - ).marginOnly(right: 10.0), + _buildClientAvatar().marginOnly(right: 10.0), Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.start, @@ -582,6 +566,35 @@ class _CmHeaderState extends State<_CmHeader> @override bool get wantKeepAlive => true; + + Widget _buildClientAvatar() { + return buildAvatarWidget( + avatar: client.avatar, + size: 70, + borderRadius: 15, + fallback: _buildInitialAvatar(), + )!; + } + + Widget _buildInitialAvatar() { + return Container( + width: 70, + height: 70, + alignment: Alignment.center, + decoration: BoxDecoration( + color: str2color(client.name), + borderRadius: BorderRadius.circular(15.0), + ), + child: Text( + client.name.isNotEmpty ? client.name[0] : '?', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.white, + fontSize: 55, + ), + ), + ); + } } class _PrivilegeBoard extends StatefulWidget { diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index d2a6ed8a8..d0a7b573e 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -841,13 +841,7 @@ class ClientInfo extends StatelessWidget { flex: -1, child: Padding( padding: const EdgeInsets.only(right: 12), - child: CircleAvatar( - backgroundColor: str2color( - client.name, - Theme.of(context).brightness == Brightness.light - ? 255 - : 150), - child: Text(client.name[0])))), + child: _buildAvatar(context))), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -860,6 +854,20 @@ class ClientInfo extends StatelessWidget { ), ])); } + + Widget _buildAvatar(BuildContext context) { + final fallback = CircleAvatar( + backgroundColor: str2color( + client.name, + Theme.of(context).brightness == Brightness.light ? 255 : 150), + child: Text(client.name.isNotEmpty ? client.name[0] : '?'), + ); + return buildAvatarWidget( + avatar: client.avatar, + size: 40, + fallback: fallback, + )!; + } } void androidChannelInit() { diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index afd3422d7..e047344ae 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -689,7 +689,15 @@ class _SettingsState extends State with WidgetsBindingObserver { title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty ? translate('Login') : '${translate('Logout')} (${gFFI.userModel.accountLabelWithHandle})')), - leading: Icon(Icons.person), + leading: Obx(() { + final avatar = bind.mainResolveAvatarUrl( + avatar: gFFI.userModel.avatar.value); + return buildAvatarWidget( + avatar: avatar, + size: 40, + ) ?? + Icon(Icons.person); + }), onPressed: (context) { if (gFFI.userModel.userName.value.isEmpty) { loginDialog(); diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 8ead158ac..5892ed0fe 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -820,6 +820,7 @@ class Client { bool isTerminal = false; String portForward = ""; String name = ""; + String avatar = ""; String peerId = ""; // peer user's id,show at app bool keyboard = false; bool clipboard = false; @@ -847,6 +848,7 @@ class Client { isTerminal = json['is_terminal'] ?? false; portForward = json['port_forward']; name = json['name']; + avatar = json['avatar'] ?? ''; peerId = json['peer_id']; keyboard = json['keyboard']; clipboard = json['clipboard']; @@ -870,6 +872,7 @@ class Client { data['is_terminal'] = isTerminal; data['port_forward'] = portForward; data['name'] = name; + data['avatar'] = avatar; data['peer_id'] = peerId; data['keyboard'] = keyboard; data['clipboard'] = clipboard; diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart index c850c4cf6..cecb58eaa 100644 --- a/flutter/lib/models/user_model.dart +++ b/flutter/lib/models/user_model.dart @@ -17,6 +17,7 @@ bool refreshingUser = false; class UserModel { final RxString userName = ''.obs; final RxString displayName = ''.obs; + final RxString avatar = ''.obs; final RxBool isAdmin = false.obs; final RxString networkError = ''.obs; bool get isLogin => userName.isNotEmpty; @@ -33,6 +34,7 @@ class UserModel { } return '$preferred (@$username)'; } + WeakReference parent; UserModel(this.parent) { @@ -114,6 +116,7 @@ class UserModel { if (userInfo != null) { userName.value = (userInfo['name'] ?? '').toString(); displayName.value = (userInfo['display_name'] ?? '').toString(); + avatar.value = (userInfo['avatar'] ?? '').toString(); } } @@ -126,11 +129,13 @@ class UserModel { } userName.value = ''; displayName.value = ''; + avatar.value = ''; } _parseAndUpdateUser(UserPayload user) { userName.value = user.name; displayName.value = user.displayName; + avatar.value = user.avatar; isAdmin.value = user.isAdmin; bind.mainSetLocalOption(key: 'user_info', value: jsonEncode(user)); if (isWeb) { diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 4a4e89233..66191d004 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -2034,5 +2034,9 @@ class RustdeskImpl { return false; } + String mainResolveAvatarUrl({required String avatar, dynamic hint}) { + return js.context.callMethod('getByName', ['resolve_avatar_url', avatar])?.toString() ?? avatar; + } + void dispose() {} } diff --git a/src/client.rs b/src/client.rs index cb4ed3a24..8ea70898f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -33,7 +33,7 @@ use crate::{ create_symmetric_key_msg, decode_id_pk, get_rs_pk, is_keyboard_mode_supported, kcp_stream::KcpStream, secure_tcp, - ui_interface::{get_builtin_option, use_texture_render}, + ui_interface::{get_builtin_option, resolve_avatar_url, use_texture_render}, ui_session_interface::{InvokeUiSession, Session}, }; #[cfg(feature = "unix-file-copy-paste")] @@ -2625,6 +2625,20 @@ impl LoginConfigHandler { } else { (my_id, self.id.clone()) }; + let mut avatar = get_builtin_option(keys::OPTION_AVATAR); + if avatar.is_empty() { + avatar = serde_json::from_str::(&LocalConfig::get_option( + "user_info", + )) + .ok() + .and_then(|x| { + x.get("avatar") + .and_then(|x| x.as_str()) + .map(|x| x.trim().to_owned()) + }) + .unwrap_or_default(); + } + avatar = resolve_avatar_url(avatar); let mut display_name = get_builtin_option(keys::OPTION_DISPLAY_NAME); if display_name.is_empty() { display_name = @@ -2684,6 +2698,7 @@ impl LoginConfigHandler { }) .into(), hwid, + avatar, ..Default::default() }; match self.conn_type { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index ed13a7624..551ad799f 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1101,6 +1101,10 @@ pub fn main_get_api_server() -> String { get_api_server() } +pub fn main_resolve_avatar_url(avatar: String) -> SyncReturn { + SyncReturn(resolve_avatar_url(avatar)) +} + pub fn main_http_request(url: String, method: String, body: Option, header: String) { http_request(url, method, body, header) } diff --git a/src/hbbs_http/account.rs b/src/hbbs_http/account.rs index 6644aee28..8e6141200 100644 --- a/src/hbbs_http/account.rs +++ b/src/hbbs_http/account.rs @@ -17,6 +17,7 @@ lazy_static::lazy_static! { const QUERY_INTERVAL_SECS: f32 = 1.0; const QUERY_TIMEOUT_SECS: u64 = 60 * 3; + const REQUESTING_ACCOUNT_AUTH: &str = "Requesting account auth"; const WAITING_ACCOUNT_AUTH: &str = "Waiting account auth"; const LOGIN_ACCOUNT_AUTH: &str = "Login account auth"; @@ -82,6 +83,8 @@ pub struct UserPayload { #[serde(default)] pub display_name: Option, #[serde(default)] + pub avatar: Option, + #[serde(default)] pub email: Option, #[serde(default)] pub note: Option, @@ -273,6 +276,7 @@ impl OidcSession { serde_json::json!({ "name": auth_body.user.name, "display_name": auth_body.user.display_name, + "avatar": auth_body.user.avatar, "status": auth_body.user.status }) .to_string(), diff --git a/src/ipc.rs b/src/ipc.rs index a5d27ba8a..891ec81dd 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -226,6 +226,7 @@ pub enum Data { is_terminal: bool, peer_id: String, name: String, + avatar: String, authorized: bool, port_forward: String, keyboard: bool, @@ -1583,6 +1584,6 @@ mod test { #[test] fn verify_ffi_enum_data_size() { println!("{}", std::mem::size_of::()); - assert!(std::mem::size_of::() <= 96); + assert!(std::mem::size_of::() <= 120); } } diff --git a/src/server/connection.rs b/src/server/connection.rs index 033aac0ce..1ffb1a25e 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1877,6 +1877,7 @@ impl Connection { port_forward: self.port_forward_address.clone(), peer_id, name, + avatar: self.lr.avatar.clone(), authorized, keyboard: self.keyboard, clipboard: self.clipboard, diff --git a/src/ui/cm.css b/src/ui/cm.css index baa774309..ba6de887b 100644 --- a/src/ui/cm.css +++ b/src/ui/cm.css @@ -57,6 +57,11 @@ div.icon { font-weight: bold; } +img.icon { + size: 96px; + border-radius: 8px; +} + div.id { @ELLIPSIS; color: color(green-blue); diff --git a/src/ui/cm.rs b/src/ui/cm.rs index 92cd2e2f2..15b7b9435 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -28,6 +28,7 @@ impl InvokeUiCM for SciterHandler { client.port_forward.clone(), client.peer_id.clone(), client.name.clone(), + client.avatar.clone(), client.authorized, client.keyboard, client.clipboard, diff --git a/src/ui/cm.tis b/src/ui/cm.tis index 0b0165b73..a06fb9ff8 100644 --- a/src/ui/cm.tis +++ b/src/ui/cm.tis @@ -42,9 +42,11 @@ class Body: Reactor.Component return
    + {c.avatar ? + :
    {c.name[0].toUpperCase()} -
    +
    }
    {c.name}
    ({c.peer_id})
    @@ -366,7 +368,7 @@ function bring_to_top(idx=-1) { } } -handler.addConnection = function(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input) { +handler.addConnection = function(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, restart, recording, block_input) { stdout.println("new connection #" + id + ": " + peer_id); var conn; connections.map(function(c) { @@ -385,6 +387,7 @@ handler.addConnection = function(id, is_file_transfer, is_view_camera, is_termin conn = { id: id, is_file_transfer: is_file_transfer, is_view_camera: is_view_camera, is_terminal: is_terminal, peer_id: peer_id, port_forward: port_forward, + avatar: avatar, name: name, authorized: authorized, time: new Date(), now: new Date(), keyboard: keyboard, clipboard: clipboard, msgs: [], unreaded: 0, audio: audio, file: file, restart: restart, recording: recording, diff --git a/src/ui/index.tis b/src/ui/index.tis index edd69312e..5853fe3e2 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -1451,6 +1451,9 @@ function set_local_user_info(user) { if (user.display_name) { user_info.display_name = user.display_name; } + if (user.avatar) { + user_info.avatar = user.avatar; + } if (user.status) { user_info.status = user.status; } diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 4e688429f..75e724007 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -134,6 +134,7 @@ pub struct Client { pub is_terminal: bool, pub port_forward: String, pub name: String, + pub avatar: String, pub peer_id: String, pub keyboard: bool, pub clipboard: bool, @@ -220,6 +221,7 @@ impl ConnectionManager { port_forward: String, peer_id: String, name: String, + avatar: String, authorized: bool, keyboard: bool, clipboard: bool, @@ -240,6 +242,7 @@ impl ConnectionManager { is_terminal, port_forward, name: name.clone(), + avatar, peer_id: peer_id.clone(), keyboard, clipboard, @@ -500,9 +503,9 @@ impl IpcTaskRunner { } Ok(Some(data)) => { match data { - Data::Login{id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording, block_input, from_switch} => { + Data::Login{id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording, block_input, from_switch} => { log::debug!("conn_id: {}", id); - self.cm.add_connection(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, from_switch, self.tx.clone()); + self.cm.add_connection(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, from_switch, self.tx.clone()); self.conn_id = id; #[cfg(target_os = "windows")] { @@ -823,6 +826,7 @@ pub async fn start_listen( port_forward, peer_id, name, + avatar, authorized, keyboard, clipboard, @@ -843,6 +847,7 @@ pub async fn start_listen( port_forward, peer_id, name, + avatar, authorized, keyboard, clipboard, diff --git a/src/ui_interface.rs b/src/ui_interface.rs index c5f158c9d..49098f2db 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -245,7 +245,20 @@ pub fn get_builtin_option(key: &str) -> String { #[inline] pub fn set_local_option(key: String, value: String) { - LocalConfig::set_option(key.clone(), value.clone()); + LocalConfig::set_option(key.clone(), value); +} + +/// Resolve relative avatar path (e.g. "/avatar/xxx") to absolute URL +/// by prepending the API server address. +pub fn resolve_avatar_url(avatar: String) -> String { + let avatar = avatar.trim().to_owned(); + if avatar.starts_with('/') { + let api_server = get_api_server(); + if !api_server.is_empty() { + return format!("{}{}", api_server.trim_end_matches('/'), avatar); + } + } + avatar } #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] From 1abc897c451c8b5bbff3792509a7fef9d12f2ce3 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 5 Mar 2026 12:30:40 +0800 Subject: [PATCH 168/277] fix avatar fallback (#14458) * fix avatar fallback Signed-off-by: 21pages * fix(ui): improve avatar fallback handling and layout consistency - Always show spacing in account section regardless of avatar presence - Handle null return from buildAvatarWidget with proper fallback - Adjust mobile settings avatar size to 28 Signed-off-by: 21pages --------- Signed-off-by: 21pages --- flutter/lib/desktop/pages/desktop_setting_page.dart | 2 +- flutter/lib/desktop/pages/server_page.dart | 11 ++++++----- flutter/lib/mobile/pages/server_page.dart | 12 ++++++------ flutter/lib/mobile/pages/settings_page.dart | 12 ++++++++---- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index bde40cf19..82212d191 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -2039,7 +2039,7 @@ class _AccountState extends State<_Account> { return Row( children: [ if (avatarWidget != null) avatarWidget, - if (avatarWidget != null) const SizedBox(width: 12), + const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index ea37c95e4..7d48452a8 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -569,11 +569,12 @@ class _CmHeaderState extends State<_CmHeader> Widget _buildClientAvatar() { return buildAvatarWidget( - avatar: client.avatar, - size: 70, - borderRadius: 15, - fallback: _buildInitialAvatar(), - )!; + avatar: client.avatar, + size: 70, + borderRadius: 15, + fallback: _buildInitialAvatar(), + ) ?? + _buildInitialAvatar(); } Widget _buildInitialAvatar() { diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index d0a7b573e..54406ff2e 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -857,16 +857,16 @@ class ClientInfo extends StatelessWidget { Widget _buildAvatar(BuildContext context) { final fallback = CircleAvatar( - backgroundColor: str2color( - client.name, + backgroundColor: str2color(client.name, Theme.of(context).brightness == Brightness.light ? 255 : 150), child: Text(client.name.isNotEmpty ? client.name[0] : '?'), ); return buildAvatarWidget( - avatar: client.avatar, - size: 40, - fallback: fallback, - )!; + avatar: client.avatar, + size: 40, + fallback: fallback, + ) ?? + fallback; } } diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index e047344ae..509260636 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -617,7 +617,7 @@ class _SettingsState extends State with WidgetsBindingObserver { onToggle: (bool v) async { await mainSetLocalBoolOption(kOptionEnableShowTerminalExtraKeys, v); final newValue = - mainGetLocalBoolOptionSync(kOptionEnableShowTerminalExtraKeys); + mainGetLocalBoolOptionSync(kOptionEnableShowTerminalExtraKeys); setState(() { _showTerminalExtraKeys = newValue; }); @@ -694,7 +694,9 @@ class _SettingsState extends State with WidgetsBindingObserver { avatar: gFFI.userModel.avatar.value); return buildAvatarWidget( avatar: avatar, - size: 40, + size: 28, + borderRadius: null, + fallback: Icon(Icons.person), ) ?? Icon(Icons.person); }), @@ -837,10 +839,12 @@ class _SettingsState extends State with WidgetsBindingObserver { ), if (!incomingOnly) SettingsTile.switchTile( - title: Text(translate('keep-awake-during-outgoing-sessions-label')), + title: + Text(translate('keep-awake-during-outgoing-sessions-label')), initialValue: _preventSleepWhileConnected, onToggle: (v) async { - await mainSetLocalBoolOption(kOptionKeepAwakeDuringOutgoingSessions, v); + await mainSetLocalBoolOption( + kOptionKeepAwakeDuringOutgoingSessions, v); setState(() { _preventSleepWhileConnected = v; }); From 0d3016fcd82545a3a759d29f052463599fba0d3c Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 5 Mar 2026 23:10:39 +0800 Subject: [PATCH 169/277] fix(flutter): reduce accidental horizontal trackpad scrolling during vertical pan (#14460) * fix(flutter): reduce accidental horizontal trackpad scrolling during vertical pan Signed-off-by: fufesou * refact: comments Signed-off-by: fufesou --------- Signed-off-by: fufesou --- flutter/lib/models/input_model.dart | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 628b27fb2..675a95e42 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -348,6 +348,12 @@ class InputModel { final _trackpadAdjustPeerLinux = 0.06; // This is an experience value. final _trackpadAdjustMacToWin = 2.50; + // Ignore directional locking for very small deltas on both axes (including + // tiny single-axis movement) to avoid over-filtering near zero. + static const double _trackpadAxisNoiseThreshold = 0.2; + // Lock to dominant axis only when one axis is clearly stronger. + // 1.6 means the dominant axis must be >= 60% larger than the other. + static const double _trackpadAxisLockRatio = 1.6; int _trackpadSpeed = kDefaultTrackpadSpeed; double _trackpadSpeedInner = kDefaultTrackpadSpeed / 100.0; var _trackpadScrollUnsent = Offset.zero; @@ -1172,6 +1178,7 @@ class InputModel { if (isMacOS && peerPlatform == kPeerPlatformWindows) { delta *= _trackpadAdjustMacToWin; } + delta = _filterTrackpadDeltaAxis(delta); _trackpadLastDelta = delta; var x = delta.dx.toInt(); @@ -1204,6 +1211,24 @@ class InputModel { } } + Offset _filterTrackpadDeltaAxis(Offset delta) { + final absDx = delta.dx.abs(); + final absDy = delta.dy.abs(); + // Keep diagonal intent when movement is tiny on both axes. + if (absDx < _trackpadAxisNoiseThreshold && + absDy < _trackpadAxisNoiseThreshold) { + return delta; + } + // Dominant-axis lock to reduce accidental cross-axis scrolling noise. + if (absDy >= absDx * _trackpadAxisLockRatio) { + return Offset(0, delta.dy); + } + if (absDx >= absDy * _trackpadAxisLockRatio) { + return Offset(delta.dx, 0); + } + return delta; + } + void _scheduleFling(double x, double y, int delay) { if (isViewCamera) return; if ((x == 0 && y == 0) || _stopFling) { From db3f5fe816896e61a40400a78da9e9418e83b703 Mon Sep 17 00:00:00 2001 From: layla <111667698+04cb@users.noreply.github.com> Date: Sun, 8 Mar 2026 19:18:59 +0800 Subject: [PATCH 170/277] Fix typo: Rustdesk to RustDesk in Russian README (#14468) --- docs/README-RU.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README-RU.md b/docs/README-RU.md index ad12e9527..928faad07 100644 --- a/docs/README-RU.md +++ b/docs/README-RU.md @@ -167,7 +167,7 @@ target/release/rustdesk - **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: графический пользовательский интерфейс на Sciter (устаревшее) - **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: сервисы аудио, буфера обмена, ввода, видео и сетевых подключений - **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: одноранговое соединение -- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: связь с [сервером Rustdesk](https://github.com/rustdesk/rustdesk-server), ожидает удаленного прямого (через TCP hole punching) или ретранслируемого соединения +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: связь с [сервером RustDesk](https://github.com/rustdesk/rustdesk-server), ожидает удаленного прямого (через TCP hole punching) или ретранслируемого соединения - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: специфичный для платформы код - **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: код Flutter для ПК-версии и мобильных устройств - **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: JavaScript для Web-клиента Flutter From fd7bcf54bdd86b7f957b3b95305da9fdaaccc650 Mon Sep 17 00:00:00 2001 From: John Fowler Date: Mon, 9 Mar 2026 14:28:37 +0100 Subject: [PATCH 171/277] Hungarian language file update (#14497) * Update Hungarian translations in hu.rs Translation of new strings and some fixes. John Fowler. * Escape quotes in Hungarian language strings Replacing Hungarian quotation marks * Update Hungarian translations for various terms Upload a new translation (hu.rs) file. * Hungarian language file correction New character strings translation, error correction. * Hungarian language file update New string translations. --- src/lang/hu.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 03b601116..85153f618 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -738,5 +738,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", "Változáslista"), ("keep-awake-during-outgoing-sessions-label", "Képernyő aktív állapotban tartása a kimenő munkamenetek során"), ("keep-awake-during-incoming-sessions-label", "Képernyő aktív állapotban tartása a bejövő munkamenetek során"), + ("Continue with {}", "Folytatás ezzel: {}"), + ("Display Name", "Kijelző név"), ].iter().cloned().collect(); } From 016a0b11416fb70dc13e621e690819ea5828d2df Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 10 Mar 2026 13:24:13 +0800 Subject: [PATCH 172/277] fix strategy cannot apply over default advanced options (#14502) Signed-off-by: 21pages --- src/hbbs_http/sync.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/hbbs_http/sync.rs b/src/hbbs_http/sync.rs index d3083acd1..1bb61943f 100644 --- a/src/hbbs_http/sync.rs +++ b/src/hbbs_http/sync.rs @@ -286,10 +286,14 @@ fn heartbeat_url() -> String { fn handle_config_options(config_options: HashMap) { let mut options = Config::get_options(); + let default_settings = config::DEFAULT_SETTINGS.read().unwrap().clone(); config_options .iter() .map(|(k, v)| { - if v.is_empty() { + // Priority: user config > default advanced options. + // Only when default advanced options are also empty, remove user option (fallback to built-in default); + // otherwise insert an empty value so user config remains present. + if v.is_empty() && default_settings.get(k).map_or("", |v| v).is_empty() { options.remove(k); } else { options.insert(k.to_string(), v.to_string()); From b3f43f55c1c00f287b8baf22152f3d1b88b51fb6 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Wed, 11 Mar 2026 18:28:37 +0800 Subject: [PATCH 173/277] fix(mobile): restore canvas offset after hidding the soft keyboard (#14506) * fix(mobile): restore canvas offset after hidding the soft keyboard Signed-off-by: fufesou * fix(mobile): ingore mobileFocusCanvasCursor in didChangeMetrics Signed-off-by: fufesou * fix(mobile): remove unused code Signed-off-by: fufesou * refact(mobile): simple refactor Signed-off-by: fufesou * fix(mobile): restore canvas, cancel focus timer Signed-off-by: fufesou --------- Signed-off-by: fufesou --- flutter/lib/mobile/pages/remote_page.dart | 24 -------------- flutter/lib/models/model.dart | 40 +++++++++++++++++++++-- 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index b379a5591..9102d163c 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -65,9 +64,7 @@ class _RemotePageState extends State with WidgetsBindingObserver { bool _showGestureHelp = false; String _value = ''; Orientation? _currentOrientation; - double _viewInsetsBottom = 0; final _uniqueKey = UniqueKey(); - Timer? _timerDidChangeMetrics; Timer? _iosKeyboardWorkaroundTimer; final _blockableOverlayState = BlockableOverlayState(); @@ -140,7 +137,6 @@ class _RemotePageState extends State with WidgetsBindingObserver { _physicalFocusNode.dispose(); await gFFI.close(); _timer?.cancel(); - _timerDidChangeMetrics?.cancel(); _iosKeyboardWorkaroundTimer?.cancel(); gFFI.dialogManager.dismissAll(); await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, @@ -167,26 +163,6 @@ class _RemotePageState extends State with WidgetsBindingObserver { gFFI.invokeMethod("try_sync_clipboard"); } - @override - void didChangeMetrics() { - // If the soft keyboard is visible and the canvas has been changed(panned or scaled) - // Don't try reset the view style and focus the cursor. - if (gFFI.cursorModel.lastKeyboardIsVisible && - gFFI.canvasModel.isMobileCanvasChanged) { - return; - } - - final newBottom = MediaQueryData.fromView(ui.window).viewInsets.bottom; - _timerDidChangeMetrics?.cancel(); - _timerDidChangeMetrics = Timer(Duration(milliseconds: 100), () async { - // We need this comparation because poping up the floating action will also trigger `didChangeMetrics()`. - if (newBottom != _viewInsetsBottom) { - gFFI.canvasModel.mobileFocusCanvasCursor(); - _viewInsetsBottom = newBottom; - } - }); - } - // to-do: It should be better to use transparent color instead of the bgColor. // But for now, the transparent color will cause the canvas to be white. // I'm sure that the white color is caused by the Overlay widget in BlockableOverlay. diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index ff298c380..de41a2a78 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -2152,6 +2152,9 @@ class CanvasModel with ChangeNotifier { ViewStyle _lastViewStyle = ViewStyle.defaultViewStyle(); Timer? _timerMobileFocusCanvasCursor; + Timer? _timerMobileRestoreCanvasOffset; + Offset? _offsetBeforeMobileSoftKeyboard; + double? _scaleBeforeMobileSoftKeyboard; // `isMobileCanvasChanged` is used to avoid canvas reset when changing the input method // after showing the soft keyboard. @@ -2639,6 +2642,9 @@ class CanvasModel with ChangeNotifier { _scale = 1.0; _lastViewStyle = ViewStyle.defaultViewStyle(); _timerMobileFocusCanvasCursor?.cancel(); + _timerMobileRestoreCanvasOffset?.cancel(); + _offsetBeforeMobileSoftKeyboard = null; + _scaleBeforeMobileSoftKeyboard = null; } updateScrollPercent() { @@ -2667,6 +2673,31 @@ class CanvasModel with ChangeNotifier { }); } + void saveMobileOffsetBeforeSoftKeyboard() { + _timerMobileRestoreCanvasOffset?.cancel(); + _offsetBeforeMobileSoftKeyboard = Offset(_x, _y); + _scaleBeforeMobileSoftKeyboard = _scale; + } + + void restoreMobileOffsetAfterSoftKeyboard() { + _timerMobileRestoreCanvasOffset?.cancel(); + _timerMobileFocusCanvasCursor?.cancel(); + final targetOffset = _offsetBeforeMobileSoftKeyboard; + final targetScale = _scaleBeforeMobileSoftKeyboard; + if (targetOffset == null || targetScale == null) { + return; + } + _timerMobileRestoreCanvasOffset = Timer(Duration(milliseconds: 100), () { + updateSize(); + _x = targetOffset.dx; + _y = targetOffset.dy; + _scale = targetScale; + _offsetBeforeMobileSoftKeyboard = null; + _scaleBeforeMobileSoftKeyboard = null; + notifyListeners(); + }); + } + // mobile only // Move the canvas to make the cursor visible(center) on the screen. void _moveToCenterCursor() { @@ -2919,8 +2950,13 @@ class CursorModel with ChangeNotifier { _lastIsBlocked = true; } if (isMobile && _lastKeyboardIsVisible != keyboardIsVisible) { - parent.target?.canvasModel.mobileFocusCanvasCursor(); - parent.target?.canvasModel.isMobileCanvasChanged = false; + if (keyboardIsVisible) { + parent.target?.canvasModel.saveMobileOffsetBeforeSoftKeyboard(); + parent.target?.canvasModel.mobileFocusCanvasCursor(); + parent.target?.canvasModel.isMobileCanvasChanged = false; + } else { + parent.target?.canvasModel.restoreMobileOffsetAfterSoftKeyboard(); + } } _lastKeyboardIsVisible = keyboardIsVisible; } From 682e347be0990f5e19052020337ab9b67ae1d6e7 Mon Sep 17 00:00:00 2001 From: Vasyl Gello Date: Thu, 12 Mar 2026 10:52:33 +0200 Subject: [PATCH 174/277] Bump Android NDK to r28c (#13685) Signed-off-by: Vasyl Gello Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> --- .github/workflows/flutter-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index eb101400d..263bd67dc 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -40,7 +40,7 @@ env: VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b" ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" # 2025.01.13, got "/opt/artifacts/vcpkg/vcpkg: No such file or directory" with latest version VERSION: "1.4.6" - NDK_VERSION: "r27c" + NDK_VERSION: "r28c" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}" From 96797742f27b475e018f161f6a365a9f235483e6 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 13 Mar 2026 10:42:13 +0800 Subject: [PATCH 175/277] fix https://github.com/rustdesk/rustdesk/issues/14520 --- src/core_main.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core_main.rs b/src/core_main.rs index 3119529c6..e27091927 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -213,7 +213,7 @@ pub fn core_main() -> Option> { } Ok(false) => "Update failed!".to_string(), Ok(true) => match platform::update_me(false) { - Ok(_) => "Update successfully!".to_string(), + Ok(_) => "Updated successfully!".to_string(), Err(err) => { log::error!("Failed with error: {err}"); "Update failed!".to_string() @@ -335,8 +335,8 @@ pub fn core_main() -> Option> { log::info!("Starting update process..."); let _text = match platform::update_me() { Ok(_) => { - println!("{}", translate("Update successfully!".to_string())); - log::info!("Update successfully!"); + println!("{}", translate("Updated successfully!".to_string())); + log::info!("Updated successfully!"); } Err(err) => { eprintln!("Update failed with error: {}", err); From 1e2d2c514697dd3646330cbde9109d9001337a8e Mon Sep 17 00:00:00 2001 From: Eric Blanquer Date: Sat, 14 Mar 2026 07:48:20 +0100 Subject: [PATCH 176/277] Update tray-icon crate to fix Linux tray icon collision (#14530) Bump tray-icon from 0.14.3 to 0.21.3 which includes the fix from tauri-apps/tray-icon#290 that derives the icon id from the process id, preventing icon collisions between apps using the same crate (e.g. Synergy, R-Quick-Share). Refs: https://github.com/rustdesk/rustdesk/discussions/14165 --- Cargo.lock | 354 +++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 290 insertions(+), 64 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 06cfeeb96..febfd6b17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,6 +33,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aead" version = "0.5.2" @@ -293,8 +299,8 @@ dependencies = [ "image 0.25.1", "log", "objc2 0.5.2", - "objc2-app-kit", - "objc2-foundation", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", "parking_lot", "percent-encoding", "serde 1.0.228", @@ -637,7 +643,7 @@ dependencies = [ "cc", "cfg-if 1.0.0", "libc", - "miniz_oxide", + "miniz_oxide 0.7.4", "object", "rustc-demangle", ] @@ -860,6 +866,15 @@ dependencies = [ "objc2 0.5.2", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.4", +] + [[package]] name = "blocking" version = "1.6.1" @@ -1182,7 +1197,7 @@ dependencies = [ "js-sys", "num-traits 0.2.19", "wasm-bindgen", - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -1290,8 +1305,8 @@ dependencies = [ "lazy_static", "libc", "objc2 0.5.2", - "objc2-app-kit", - "objc2-foundation", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", "once_cell", "parking_lot", "percent-encoding", @@ -2216,6 +2231,15 @@ dependencies = [ "dirs-sys 0.4.1", ] +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -2233,7 +2257,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" dependencies = [ "libc", - "redox_users", + "redox_users 0.4.5", "winapi 0.3.9", ] @@ -2245,10 +2269,22 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.4.5", "windows-sys 0.48.0", ] +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.2", + "windows-sys 0.61.2", +] + [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -2256,7 +2292,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", - "redox_users", + "redox_users 0.4.5", "winapi 0.3.9", ] @@ -2266,6 +2302,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.9.1", + "objc2 0.6.4", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -2715,7 +2761,7 @@ dependencies = [ "flume", "half", "lebe", - "miniz_oxide", + "miniz_oxide 0.7.4", "rayon-core", "smallvec", "zune-inflate", @@ -2801,12 +2847,12 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.30" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.8.9", ] [[package]] @@ -4041,7 +4087,7 @@ dependencies = [ "gif", "jpeg-decoder", "num-traits 0.2.19", - "png", + "png 0.17.13", "qoi", "tiff", ] @@ -4055,7 +4101,7 @@ dependencies = [ "bytemuck", "byteorder", "num-traits 0.2.19", - "png", + "png 0.17.13", "tiff", ] @@ -4766,6 +4812,16 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "0.8.11" @@ -4816,21 +4872,23 @@ dependencies = [ [[package]] name = "muda" -version = "0.13.5" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b959f97c97044e4c96e32e1db292a7d594449546a3c6b77ae613dc3a5b5145" +checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" dependencies = [ - "cocoa 0.25.0", "crossbeam-channel", "dpi", "gtk", "keyboard-types", "libxdo", - "objc", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", "once_cell", - "png", - "thiserror 1.0.61", - "windows-sys 0.52.0", + "png 0.17.13", + "thiserror 2.0.17", + "windows-sys 0.60.2", ] [[package]] @@ -5374,7 +5432,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" dependencies = [ "objc-sys 0.3.5", - "objc2-encode 4.0.3", + "objc2-encode 4.1.0", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode 4.1.0", ] [[package]] @@ -5389,10 +5456,22 @@ dependencies = [ "objc2 0.5.2", "objc2-core-data", "objc2-core-image", - "objc2-foundation", + "objc2-foundation 0.2.2", "objc2-quartz-core", ] +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.9.1", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-cloud-kit" version = "0.2.2" @@ -5403,7 +5482,7 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -5414,7 +5493,7 @@ checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" dependencies = [ "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -5426,7 +5505,28 @@ dependencies = [ "bitflags 2.9.1", "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.9.1", + "dispatch2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.9.1", + "objc2-core-foundation", ] [[package]] @@ -5437,7 +5537,7 @@ checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation", + "objc2-foundation 0.2.2", "objc2-metal", ] @@ -5450,7 +5550,7 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-contacts", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -5464,9 +5564,9 @@ dependencies = [ [[package]] name = "objc2-encode" -version = "4.0.3" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] name = "objc2-foundation" @@ -5481,6 +5581,18 @@ dependencies = [ "objc2 0.5.2", ] +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.9.1", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-core-foundation", +] + [[package]] name = "objc2-link-presentation" version = "0.2.2" @@ -5489,8 +5601,8 @@ checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" dependencies = [ "block2 0.5.1", "objc2 0.5.2", - "objc2-app-kit", - "objc2-foundation", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -5502,7 +5614,7 @@ dependencies = [ "bitflags 2.9.1", "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -5514,7 +5626,7 @@ dependencies = [ "bitflags 2.9.1", "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation", + "objc2-foundation 0.2.2", "objc2-metal", ] @@ -5525,7 +5637,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" dependencies = [ "objc2 0.5.2", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -5541,7 +5653,7 @@ dependencies = [ "objc2-core-data", "objc2-core-image", "objc2-core-location", - "objc2-foundation", + "objc2-foundation 0.2.2", "objc2-link-presentation", "objc2-quartz-core", "objc2-symbols", @@ -5557,7 +5669,7 @@ checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" dependencies = [ "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -5570,7 +5682,7 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -6178,7 +6290,20 @@ dependencies = [ "crc32fast", "fdeflate", "flate2", - "miniz_oxide", + "miniz_oxide 0.7.4", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.9.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", ] [[package]] @@ -6863,6 +6988,17 @@ dependencies = [ "thiserror 1.0.61", ] +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror 2.0.17", +] + [[package]] name = "regex" version = "1.11.1" @@ -7981,8 +8117,8 @@ dependencies = [ "log", "memmap2", "objc2 0.5.2", - "objc2-app-kit", - "objc2-foundation", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", "objc2-quartz-core", "raw-window-handle 0.6.2", "redox_syscall 0.5.2", @@ -8312,7 +8448,7 @@ dependencies = [ "objc", "once_cell", "parking_lot", - "png", + "png 0.17.13", "raw-window-handle 0.6.2", "scopeguard", "tao-macros", @@ -8566,7 +8702,7 @@ dependencies = [ "bytemuck", "cfg-if 1.0.0", "log", - "png", + "png 0.17.13", "tiny-skia-path", ] @@ -8939,21 +9075,22 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.14.3" -source = "git+https://github.com/tauri-apps/tray-icon#d4078696edba67b0ab42cef67e6a421a0332c96f" +version = "0.21.3" +source = "git+https://github.com/tauri-apps/tray-icon#0a5835b0e6828e37a1f781de9c2d671ae7a939e6" dependencies = [ - "core-graphics 0.23.2", "crossbeam-channel", - "dirs 5.0.1", + "dirs 6.0.0", "libappindicator", "muda", - "objc2 0.5.2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", "once_cell", - "png", - "thiserror 1.0.61", - "windows-sys 0.52.0", + "png 0.18.1", + "thiserror 2.0.17", + "windows-sys 0.60.2", ] [[package]] @@ -10058,7 +10195,7 @@ dependencies = [ "windows-collections", "windows-core 0.61.0", "windows-future", - "windows-link", + "windows-link 0.1.1", "windows-numerics", ] @@ -10107,7 +10244,7 @@ checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ "windows-implement 0.60.0", "windows-interface 0.59.1", - "windows-link", + "windows-link 0.1.1", "windows-result 0.3.2", "windows-strings 0.4.0", ] @@ -10119,7 +10256,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32" dependencies = [ "windows-core 0.61.0", - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -10172,6 +10309,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-numerics" version = "0.2.0" @@ -10179,7 +10322,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ "windows-core 0.61.0", - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -10197,7 +10340,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" dependencies = [ - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -10217,7 +10360,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" dependencies = [ - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -10226,7 +10369,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" dependencies = [ - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -10256,6 +10399,24 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -10295,13 +10456,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows-version" version = "0.1.1" @@ -10338,6 +10516,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.32.0" @@ -10368,6 +10552,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.32.0" @@ -10398,12 +10588,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.32.0" @@ -10434,6 +10636,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.32.0" @@ -10464,6 +10672,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -10482,6 +10696,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.32.0" @@ -10512,6 +10732,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winit" version = "0.30.9" @@ -10536,8 +10762,8 @@ dependencies = [ "memmap2", "ndk 0.9.0", "objc2 0.5.2", - "objc2-app-kit", - "objc2-foundation", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", "objc2-ui-kit", "orbclient", "percent-encoding", From 0388d00ad33d3ced10e3e7d6283d6651ec0cff69 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Sat, 14 Mar 2026 14:49:55 +0800 Subject: [PATCH 177/277] Revert "Update tray-icon crate to fix Linux tray icon collision (#14530)" (#14538) This reverts commit 1e2d2c514697dd3646330cbde9109d9001337a8e. --- Cargo.lock | 354 ++++++++++------------------------------------------- 1 file changed, 64 insertions(+), 290 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index febfd6b17..06cfeeb96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,12 +33,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - [[package]] name = "aead" version = "0.5.2" @@ -299,8 +293,8 @@ dependencies = [ "image 0.25.1", "log", "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", + "objc2-app-kit", + "objc2-foundation", "parking_lot", "percent-encoding", "serde 1.0.228", @@ -643,7 +637,7 @@ dependencies = [ "cc", "cfg-if 1.0.0", "libc", - "miniz_oxide 0.7.4", + "miniz_oxide", "object", "rustc-demangle", ] @@ -866,15 +860,6 @@ dependencies = [ "objc2 0.5.2", ] -[[package]] -name = "block2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" -dependencies = [ - "objc2 0.6.4", -] - [[package]] name = "blocking" version = "1.6.1" @@ -1197,7 +1182,7 @@ dependencies = [ "js-sys", "num-traits 0.2.19", "wasm-bindgen", - "windows-link 0.1.1", + "windows-link", ] [[package]] @@ -1305,8 +1290,8 @@ dependencies = [ "lazy_static", "libc", "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", + "objc2-app-kit", + "objc2-foundation", "once_cell", "parking_lot", "percent-encoding", @@ -2231,15 +2216,6 @@ dependencies = [ "dirs-sys 0.4.1", ] -[[package]] -name = "dirs" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" -dependencies = [ - "dirs-sys 0.5.0", -] - [[package]] name = "dirs-next" version = "2.0.0" @@ -2257,7 +2233,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" dependencies = [ "libc", - "redox_users 0.4.5", + "redox_users", "winapi 0.3.9", ] @@ -2269,22 +2245,10 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", - "redox_users 0.4.5", + "redox_users", "windows-sys 0.48.0", ] -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users 0.5.2", - "windows-sys 0.61.2", -] - [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -2292,7 +2256,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", - "redox_users 0.4.5", + "redox_users", "winapi 0.3.9", ] @@ -2302,16 +2266,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" -[[package]] -name = "dispatch2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" -dependencies = [ - "bitflags 2.9.1", - "objc2 0.6.4", -] - [[package]] name = "displaydoc" version = "0.2.5" @@ -2761,7 +2715,7 @@ dependencies = [ "flume", "half", "lebe", - "miniz_oxide 0.7.4", + "miniz_oxide", "rayon-core", "smallvec", "zune-inflate", @@ -2847,12 +2801,12 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.1.9" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", - "miniz_oxide 0.8.9", + "miniz_oxide", ] [[package]] @@ -4087,7 +4041,7 @@ dependencies = [ "gif", "jpeg-decoder", "num-traits 0.2.19", - "png 0.17.13", + "png", "qoi", "tiff", ] @@ -4101,7 +4055,7 @@ dependencies = [ "bytemuck", "byteorder", "num-traits 0.2.19", - "png 0.17.13", + "png", "tiff", ] @@ -4812,16 +4766,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - [[package]] name = "mio" version = "0.8.11" @@ -4872,23 +4816,21 @@ dependencies = [ [[package]] name = "muda" -version = "0.17.1" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +checksum = "86b959f97c97044e4c96e32e1db292a7d594449546a3c6b77ae613dc3a5b5145" dependencies = [ + "cocoa 0.25.0", "crossbeam-channel", "dpi", "gtk", "keyboard-types", "libxdo", - "objc2 0.6.4", - "objc2-app-kit 0.3.2", - "objc2-core-foundation", - "objc2-foundation 0.3.2", + "objc", "once_cell", - "png 0.17.13", - "thiserror 2.0.17", - "windows-sys 0.60.2", + "png", + "thiserror 1.0.61", + "windows-sys 0.52.0", ] [[package]] @@ -5432,16 +5374,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" dependencies = [ "objc-sys 0.3.5", - "objc2-encode 4.1.0", -] - -[[package]] -name = "objc2" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" -dependencies = [ - "objc2-encode 4.1.0", + "objc2-encode 4.0.3", ] [[package]] @@ -5456,22 +5389,10 @@ dependencies = [ "objc2 0.5.2", "objc2-core-data", "objc2-core-image", - "objc2-foundation 0.2.2", + "objc2-foundation", "objc2-quartz-core", ] -[[package]] -name = "objc2-app-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" -dependencies = [ - "bitflags 2.9.1", - "objc2 0.6.4", - "objc2-core-foundation", - "objc2-foundation 0.3.2", -] - [[package]] name = "objc2-cloud-kit" version = "0.2.2" @@ -5482,7 +5403,7 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", - "objc2-foundation 0.2.2", + "objc2-foundation", ] [[package]] @@ -5493,7 +5414,7 @@ checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" dependencies = [ "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation 0.2.2", + "objc2-foundation", ] [[package]] @@ -5505,28 +5426,7 @@ dependencies = [ "bitflags 2.9.1", "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.9.1", - "dispatch2", - "objc2 0.6.4", -] - -[[package]] -name = "objc2-core-graphics" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" -dependencies = [ - "bitflags 2.9.1", - "objc2-core-foundation", + "objc2-foundation", ] [[package]] @@ -5537,7 +5437,7 @@ checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation 0.2.2", + "objc2-foundation", "objc2-metal", ] @@ -5550,7 +5450,7 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-contacts", - "objc2-foundation 0.2.2", + "objc2-foundation", ] [[package]] @@ -5564,9 +5464,9 @@ dependencies = [ [[package]] name = "objc2-encode" -version = "4.1.0" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" +checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" [[package]] name = "objc2-foundation" @@ -5581,18 +5481,6 @@ dependencies = [ "objc2 0.5.2", ] -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "bitflags 2.9.1", - "block2 0.6.2", - "objc2 0.6.4", - "objc2-core-foundation", -] - [[package]] name = "objc2-link-presentation" version = "0.2.2" @@ -5601,8 +5489,8 @@ checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" dependencies = [ "block2 0.5.1", "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", + "objc2-app-kit", + "objc2-foundation", ] [[package]] @@ -5614,7 +5502,7 @@ dependencies = [ "bitflags 2.9.1", "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation 0.2.2", + "objc2-foundation", ] [[package]] @@ -5626,7 +5514,7 @@ dependencies = [ "bitflags 2.9.1", "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation 0.2.2", + "objc2-foundation", "objc2-metal", ] @@ -5637,7 +5525,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" dependencies = [ "objc2 0.5.2", - "objc2-foundation 0.2.2", + "objc2-foundation", ] [[package]] @@ -5653,7 +5541,7 @@ dependencies = [ "objc2-core-data", "objc2-core-image", "objc2-core-location", - "objc2-foundation 0.2.2", + "objc2-foundation", "objc2-link-presentation", "objc2-quartz-core", "objc2-symbols", @@ -5669,7 +5557,7 @@ checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" dependencies = [ "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation 0.2.2", + "objc2-foundation", ] [[package]] @@ -5682,7 +5570,7 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", - "objc2-foundation 0.2.2", + "objc2-foundation", ] [[package]] @@ -6290,20 +6178,7 @@ dependencies = [ "crc32fast", "fdeflate", "flate2", - "miniz_oxide 0.7.4", -] - -[[package]] -name = "png" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" -dependencies = [ - "bitflags 2.9.1", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide 0.8.9", + "miniz_oxide", ] [[package]] @@ -6988,17 +6863,6 @@ dependencies = [ "thiserror 1.0.61", ] -[[package]] -name = "redox_users" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" -dependencies = [ - "getrandom 0.2.15", - "libredox", - "thiserror 2.0.17", -] - [[package]] name = "regex" version = "1.11.1" @@ -8117,8 +7981,8 @@ dependencies = [ "log", "memmap2", "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", + "objc2-app-kit", + "objc2-foundation", "objc2-quartz-core", "raw-window-handle 0.6.2", "redox_syscall 0.5.2", @@ -8448,7 +8312,7 @@ dependencies = [ "objc", "once_cell", "parking_lot", - "png 0.17.13", + "png", "raw-window-handle 0.6.2", "scopeguard", "tao-macros", @@ -8702,7 +8566,7 @@ dependencies = [ "bytemuck", "cfg-if 1.0.0", "log", - "png 0.17.13", + "png", "tiny-skia-path", ] @@ -9075,22 +8939,21 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.21.3" -source = "git+https://github.com/tauri-apps/tray-icon#0a5835b0e6828e37a1f781de9c2d671ae7a939e6" +version = "0.14.3" +source = "git+https://github.com/tauri-apps/tray-icon#d4078696edba67b0ab42cef67e6a421a0332c96f" dependencies = [ + "core-graphics 0.23.2", "crossbeam-channel", - "dirs 6.0.0", + "dirs 5.0.1", "libappindicator", "muda", - "objc2 0.6.4", - "objc2-app-kit 0.3.2", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation 0.3.2", + "objc2 0.5.2", + "objc2-app-kit", + "objc2-foundation", "once_cell", - "png 0.18.1", - "thiserror 2.0.17", - "windows-sys 0.60.2", + "png", + "thiserror 1.0.61", + "windows-sys 0.52.0", ] [[package]] @@ -10195,7 +10058,7 @@ dependencies = [ "windows-collections", "windows-core 0.61.0", "windows-future", - "windows-link 0.1.1", + "windows-link", "windows-numerics", ] @@ -10244,7 +10107,7 @@ checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ "windows-implement 0.60.0", "windows-interface 0.59.1", - "windows-link 0.1.1", + "windows-link", "windows-result 0.3.2", "windows-strings 0.4.0", ] @@ -10256,7 +10119,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32" dependencies = [ "windows-core 0.61.0", - "windows-link 0.1.1", + "windows-link", ] [[package]] @@ -10309,12 +10172,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - [[package]] name = "windows-numerics" version = "0.2.0" @@ -10322,7 +10179,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ "windows-core 0.61.0", - "windows-link 0.1.1", + "windows-link", ] [[package]] @@ -10340,7 +10197,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" dependencies = [ - "windows-link 0.1.1", + "windows-link", ] [[package]] @@ -10360,7 +10217,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" dependencies = [ - "windows-link 0.1.1", + "windows-link", ] [[package]] @@ -10369,7 +10226,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" dependencies = [ - "windows-link 0.1.1", + "windows-link", ] [[package]] @@ -10399,24 +10256,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link 0.2.1", -] - [[package]] name = "windows-targets" version = "0.42.2" @@ -10456,30 +10295,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link 0.2.1", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - [[package]] name = "windows-version" version = "0.1.1" @@ -10516,12 +10338,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.32.0" @@ -10552,12 +10368,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.32.0" @@ -10588,24 +10398,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.32.0" @@ -10636,12 +10434,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.32.0" @@ -10672,12 +10464,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -10696,12 +10482,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.32.0" @@ -10732,12 +10512,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "winit" version = "0.30.9" @@ -10762,8 +10536,8 @@ dependencies = [ "memmap2", "ndk 0.9.0", "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", + "objc2-app-kit", + "objc2-foundation", "objc2-ui-kit", "orbclient", "percent-encoding", From e3b6e4eaf09e6d17c7fc3fdbd10cd309101b537d Mon Sep 17 00:00:00 2001 From: rustdesk Date: Sat, 14 Mar 2026 15:54:54 +0800 Subject: [PATCH 178/277] update tray icon crate to fix icon conflict --- Cargo.lock | 354 +++++++++++++++++++++++++++++++++++++++++++---------- Cargo.toml | 2 +- 2 files changed, 291 insertions(+), 65 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 06cfeeb96..febfd6b17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,6 +33,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aead" version = "0.5.2" @@ -293,8 +299,8 @@ dependencies = [ "image 0.25.1", "log", "objc2 0.5.2", - "objc2-app-kit", - "objc2-foundation", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", "parking_lot", "percent-encoding", "serde 1.0.228", @@ -637,7 +643,7 @@ dependencies = [ "cc", "cfg-if 1.0.0", "libc", - "miniz_oxide", + "miniz_oxide 0.7.4", "object", "rustc-demangle", ] @@ -860,6 +866,15 @@ dependencies = [ "objc2 0.5.2", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.4", +] + [[package]] name = "blocking" version = "1.6.1" @@ -1182,7 +1197,7 @@ dependencies = [ "js-sys", "num-traits 0.2.19", "wasm-bindgen", - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -1290,8 +1305,8 @@ dependencies = [ "lazy_static", "libc", "objc2 0.5.2", - "objc2-app-kit", - "objc2-foundation", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", "once_cell", "parking_lot", "percent-encoding", @@ -2216,6 +2231,15 @@ dependencies = [ "dirs-sys 0.4.1", ] +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -2233,7 +2257,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" dependencies = [ "libc", - "redox_users", + "redox_users 0.4.5", "winapi 0.3.9", ] @@ -2245,10 +2269,22 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.4.5", "windows-sys 0.48.0", ] +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.2", + "windows-sys 0.61.2", +] + [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -2256,7 +2292,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", - "redox_users", + "redox_users 0.4.5", "winapi 0.3.9", ] @@ -2266,6 +2302,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.9.1", + "objc2 0.6.4", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -2715,7 +2761,7 @@ dependencies = [ "flume", "half", "lebe", - "miniz_oxide", + "miniz_oxide 0.7.4", "rayon-core", "smallvec", "zune-inflate", @@ -2801,12 +2847,12 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.30" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.8.9", ] [[package]] @@ -4041,7 +4087,7 @@ dependencies = [ "gif", "jpeg-decoder", "num-traits 0.2.19", - "png", + "png 0.17.13", "qoi", "tiff", ] @@ -4055,7 +4101,7 @@ dependencies = [ "bytemuck", "byteorder", "num-traits 0.2.19", - "png", + "png 0.17.13", "tiff", ] @@ -4766,6 +4812,16 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "0.8.11" @@ -4816,21 +4872,23 @@ dependencies = [ [[package]] name = "muda" -version = "0.13.5" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b959f97c97044e4c96e32e1db292a7d594449546a3c6b77ae613dc3a5b5145" +checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" dependencies = [ - "cocoa 0.25.0", "crossbeam-channel", "dpi", "gtk", "keyboard-types", "libxdo", - "objc", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", "once_cell", - "png", - "thiserror 1.0.61", - "windows-sys 0.52.0", + "png 0.17.13", + "thiserror 2.0.17", + "windows-sys 0.60.2", ] [[package]] @@ -5374,7 +5432,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" dependencies = [ "objc-sys 0.3.5", - "objc2-encode 4.0.3", + "objc2-encode 4.1.0", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode 4.1.0", ] [[package]] @@ -5389,10 +5456,22 @@ dependencies = [ "objc2 0.5.2", "objc2-core-data", "objc2-core-image", - "objc2-foundation", + "objc2-foundation 0.2.2", "objc2-quartz-core", ] +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.9.1", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-cloud-kit" version = "0.2.2" @@ -5403,7 +5482,7 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -5414,7 +5493,7 @@ checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" dependencies = [ "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -5426,7 +5505,28 @@ dependencies = [ "bitflags 2.9.1", "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.9.1", + "dispatch2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.9.1", + "objc2-core-foundation", ] [[package]] @@ -5437,7 +5537,7 @@ checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation", + "objc2-foundation 0.2.2", "objc2-metal", ] @@ -5450,7 +5550,7 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-contacts", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -5464,9 +5564,9 @@ dependencies = [ [[package]] name = "objc2-encode" -version = "4.0.3" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] name = "objc2-foundation" @@ -5481,6 +5581,18 @@ dependencies = [ "objc2 0.5.2", ] +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.9.1", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-core-foundation", +] + [[package]] name = "objc2-link-presentation" version = "0.2.2" @@ -5489,8 +5601,8 @@ checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" dependencies = [ "block2 0.5.1", "objc2 0.5.2", - "objc2-app-kit", - "objc2-foundation", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -5502,7 +5614,7 @@ dependencies = [ "bitflags 2.9.1", "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -5514,7 +5626,7 @@ dependencies = [ "bitflags 2.9.1", "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation", + "objc2-foundation 0.2.2", "objc2-metal", ] @@ -5525,7 +5637,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" dependencies = [ "objc2 0.5.2", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -5541,7 +5653,7 @@ dependencies = [ "objc2-core-data", "objc2-core-image", "objc2-core-location", - "objc2-foundation", + "objc2-foundation 0.2.2", "objc2-link-presentation", "objc2-quartz-core", "objc2-symbols", @@ -5557,7 +5669,7 @@ checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" dependencies = [ "block2 0.5.1", "objc2 0.5.2", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -5570,7 +5682,7 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-core-location", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -6178,7 +6290,20 @@ dependencies = [ "crc32fast", "fdeflate", "flate2", - "miniz_oxide", + "miniz_oxide 0.7.4", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.9.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", ] [[package]] @@ -6863,6 +6988,17 @@ dependencies = [ "thiserror 1.0.61", ] +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror 2.0.17", +] + [[package]] name = "regex" version = "1.11.1" @@ -7981,8 +8117,8 @@ dependencies = [ "log", "memmap2", "objc2 0.5.2", - "objc2-app-kit", - "objc2-foundation", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", "objc2-quartz-core", "raw-window-handle 0.6.2", "redox_syscall 0.5.2", @@ -8312,7 +8448,7 @@ dependencies = [ "objc", "once_cell", "parking_lot", - "png", + "png 0.17.13", "raw-window-handle 0.6.2", "scopeguard", "tao-macros", @@ -8566,7 +8702,7 @@ dependencies = [ "bytemuck", "cfg-if 1.0.0", "log", - "png", + "png 0.17.13", "tiny-skia-path", ] @@ -8939,21 +9075,22 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.14.3" -source = "git+https://github.com/tauri-apps/tray-icon#d4078696edba67b0ab42cef67e6a421a0332c96f" +version = "0.21.3" +source = "git+https://github.com/tauri-apps/tray-icon#0a5835b0e6828e37a1f781de9c2d671ae7a939e6" dependencies = [ - "core-graphics 0.23.2", "crossbeam-channel", - "dirs 5.0.1", + "dirs 6.0.0", "libappindicator", "muda", - "objc2 0.5.2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", "once_cell", - "png", - "thiserror 1.0.61", - "windows-sys 0.52.0", + "png 0.18.1", + "thiserror 2.0.17", + "windows-sys 0.60.2", ] [[package]] @@ -10058,7 +10195,7 @@ dependencies = [ "windows-collections", "windows-core 0.61.0", "windows-future", - "windows-link", + "windows-link 0.1.1", "windows-numerics", ] @@ -10107,7 +10244,7 @@ checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ "windows-implement 0.60.0", "windows-interface 0.59.1", - "windows-link", + "windows-link 0.1.1", "windows-result 0.3.2", "windows-strings 0.4.0", ] @@ -10119,7 +10256,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32" dependencies = [ "windows-core 0.61.0", - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -10172,6 +10309,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-numerics" version = "0.2.0" @@ -10179,7 +10322,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ "windows-core 0.61.0", - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -10197,7 +10340,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" dependencies = [ - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -10217,7 +10360,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" dependencies = [ - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -10226,7 +10369,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" dependencies = [ - "windows-link", + "windows-link 0.1.1", ] [[package]] @@ -10256,6 +10399,24 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -10295,13 +10456,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows-version" version = "0.1.1" @@ -10338,6 +10516,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.32.0" @@ -10368,6 +10552,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.32.0" @@ -10398,12 +10588,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.32.0" @@ -10434,6 +10636,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.32.0" @@ -10464,6 +10672,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -10482,6 +10696,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.32.0" @@ -10512,6 +10732,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winit" version = "0.30.9" @@ -10536,8 +10762,8 @@ dependencies = [ "memmap2", "ndk 0.9.0", "objc2 0.5.2", - "objc2-app-kit", - "objc2-foundation", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", "objc2-ui-kit", "orbclient", "percent-encoding", diff --git a/Cargo.toml b/Cargo.toml index d792d5cd5..3961e9d0b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -160,7 +160,7 @@ piet-coregraphics = "0.6" foreign-types = "0.3" [target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dependencies] -tray-icon = { git = "https://github.com/tauri-apps/tray-icon" } +tray-icon = { git = "https://github.com/tauri-apps/tray-icon", version = "0.21.3" } tao = { git = "https://github.com/rustdesk-org/tao", branch = "dev" } image = "0.24" From 02da7132e76fe85c2662a7aac42cc6754fbe51e0 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Mar 2026 18:27:39 +0800 Subject: [PATCH 179/277] Fix: note dialog not shown when closing session from reconnecting screen (#14528) * Initial plan * Fix: show ask-for-note dialog when user clicks OK on reconnecting screen (#14527) Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com> * fix: don't clear audit_guid during reconnect, clear it after connection established Signed-off-by: 21pages --------- Signed-off-by: 21pages Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com> Co-authored-by: 21pages --- flutter/lib/models/model.dart | 20 ++++++++++++++++---- src/ui_session_interface.rs | 6 ++++-- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index de41a2a78..4533f11fa 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1016,19 +1016,31 @@ class FfiModel with ChangeNotifier { showMsgBox(SessionID sessionId, String type, String title, String text, String link, bool hasRetry, OverlayDialogManager dialogManager, {bool? hasCancel}) async { - final showNoteEdit = parent.target != null && + final noteAllowed = parent.target != null && allowAskForNoteAtEndOfConnection(parent.target, false) && - (title == "Connection Error" || type == "restarting") && - !hasRetry; + (title == "Connection Error" || type == "restarting"); + final showNoteEdit = noteAllowed && !hasRetry; if (showNoteEdit) { await showConnEndAuditDialogCloseCanceled( ffi: parent.target!, type: type, title: title, text: text); closeConnection(); } else { + VoidCallback? onSubmit; + if (noteAllowed && hasRetry) { + final ffi = parent.target!; + onSubmit = () async { + _timer?.cancel(); + _timer = null; + await showConnEndAuditDialogCloseCanceled( + ffi: ffi, type: type, title: title, text: text); + closeConnection(); + }; + } msgBox(sessionId, type, title, text, link, dialogManager, hasCancel: hasCancel, reconnect: hasRetry ? reconnect : null, - reconnectTimeout: hasRetry ? _reconnects : null); + reconnectTimeout: hasRetry ? _reconnects : null, + onSubmit: onSubmit); } _timer?.cancel(); if (hasRetry) { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 9ea0cba5b..be1895e64 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1289,8 +1289,7 @@ impl Session { drop(connection_round_state_lock); let cloned = self.clone(); - *cloned.audit_guid.lock().unwrap() = String::new(); - *cloned.last_audit_note.lock().unwrap() = String::new(); + // override only if true if true == force_relay { self.lc.write().unwrap().force_relay = true; @@ -1813,6 +1812,9 @@ impl Interface for Session { ); } self.update_privacy_mode(); + // Clear audit_guid when connection is established successfully + *self.audit_guid.lock().unwrap() = String::new(); + *self.last_audit_note.lock().unwrap() = String::new(); // Save recent peers, then push event to flutter. So flutter can refresh peer page. self.lc.write().unwrap().handle_peer_info(&pi); self.set_peer_info(&pi); From 9d8df6a2260a4462dc221076950895d8d5aad167 Mon Sep 17 00:00:00 2001 From: Qusai Ismael Date: Tue, 17 Mar 2026 05:37:20 +0000 Subject: [PATCH 180/277] Fix(wayland): improve error message when xdg-desktop-portal is unavailable #12897 (#14543) * Fix: Wayland requires higher version of linux distro. Please try X11 desktop or change your OS. #12897 * refactor(wayland): optimize translation keys for binary size and improve dbus matching --- src/client.rs | 7 +++++-- src/lang/ar.rs | 5 +++-- src/lang/be.rs | 5 +++-- src/lang/bg.rs | 5 +++-- src/lang/ca.rs | 5 +++-- src/lang/cn.rs | 5 +++-- src/lang/cs.rs | 5 +++-- src/lang/da.rs | 5 +++-- src/lang/de.rs | 5 +++-- src/lang/el.rs | 5 +++-- src/lang/en.rs | 3 +++ src/lang/eo.rs | 5 +++-- src/lang/es.rs | 5 +++-- src/lang/et.rs | 5 +++-- src/lang/eu.rs | 5 +++-- src/lang/fa.rs | 5 +++-- src/lang/fi.rs | 5 +++-- src/lang/fr.rs | 5 +++-- src/lang/ge.rs | 5 +++-- src/lang/he.rs | 5 +++-- src/lang/hr.rs | 5 +++-- src/lang/hu.rs | 5 +++-- src/lang/id.rs | 5 +++-- src/lang/it.rs | 5 +++-- src/lang/ja.rs | 5 +++-- src/lang/ko.rs | 5 +++-- src/lang/kz.rs | 5 +++-- src/lang/lt.rs | 5 +++-- src/lang/lv.rs | 5 +++-- src/lang/nb.rs | 5 +++-- src/lang/nl.rs | 5 +++-- src/lang/pl.rs | 5 +++-- src/lang/pt_PT.rs | 5 +++-- src/lang/ptbr.rs | 5 +++-- src/lang/ro.rs | 5 +++-- src/lang/ru.rs | 5 +++-- src/lang/sc.rs | 5 +++-- src/lang/sk.rs | 5 +++-- src/lang/sl.rs | 5 +++-- src/lang/sq.rs | 5 +++-- src/lang/sr.rs | 5 +++-- src/lang/sv.rs | 5 +++-- src/lang/ta.rs | 5 +++-- src/lang/template.rs | 5 +++-- src/lang/th.rs | 5 +++-- src/lang/tr.rs | 5 +++-- src/lang/tw.rs | 5 +++-- src/lang/uk.rs | 5 +++-- src/lang/vi.rs | 5 +++-- src/server/wayland.rs | 14 ++++++++++---- 50 files changed, 159 insertions(+), 100 deletions(-) diff --git a/src/client.rs b/src/client.rs index 8ea70898f..527f65a12 100644 --- a/src/client.rs +++ b/src/client.rs @@ -119,10 +119,13 @@ pub const LOGIN_MSG_NO_PASSWORD_ACCESS: &str = "No Password Access"; pub const LOGIN_MSG_OFFLINE: &str = "Offline"; pub const LOGIN_SCREEN_WAYLAND: &str = "Wayland login screen is not supported"; #[cfg(target_os = "linux")] -pub const SCRAP_UBUNTU_HIGHER_REQUIRED: &str = "Wayland requires Ubuntu 21.04 or higher version."; +pub const SCRAP_UBUNTU_HIGHER_REQUIRED: &str = "ubuntu-21-04-required"; #[cfg(target_os = "linux")] pub const SCRAP_OTHER_VERSION_OR_X11_REQUIRED: &str = - "Wayland requires higher version of linux distro. Please try X11 desktop or change your OS."; + "wayland-requires-higher-linux-version"; +#[cfg(target_os = "linux")] +pub const SCRAP_XDP_PORTAL_UNAVAILABLE: &str = + "xdp-portal-unavailable"; pub const SCRAP_X11_REQUIRED: &str = "x11 expected"; pub const SCRAP_X11_REF_URL: &str = "https://rustdesk.com/docs/en/manual/linux/#x11-required"; diff --git a/src/lang/ar.rs b/src/lang/ar.rs index fc1f79c38..8af320864 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "اعدادات لوحة المفاتيح"), ("Full Access", "وصول كامل"), ("Screen Share", "مشاركة الشاشة"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland يتطلب نسخة ابونتو 21.04 او اعلى."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland يتطلب نسخة اعلى من توزيعة لينكس. الرجاء تجربة سطح مكتب X11 او غير نظام تشغيلك."), + ("ubuntu-21-04-required", "Wayland يتطلب نسخة ابونتو 21.04 او اعلى."), + ("wayland-requires-higher-linux-version", "Wayland يتطلب نسخة اعلى من توزيعة لينكس. الرجاء تجربة سطح مكتب X11 او غير نظام تشغيلك."), + ("xdp-portal-unavailable", "لاقط شاشة Wayland فشل. بوابة سطح مكتب XDG ربما توقفت عن العمل او حدث خطأ بها. جرب اعادة تشغليها عن طريق 'systemctl --user restart xdg-desktop-portal'."), ("JumpLink", "رابط القفز"), ("Please Select the screen to be shared(Operate on the peer side).", "الرجاء اختيار شاشة لمشاركتها (تعمل على جانب القرين)."), ("Show RustDesk", "عرض RustDesk"), diff --git a/src/lang/be.rs b/src/lang/be.rs index a7656782d..6735d3eff 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Налады клавіятуры"), ("Full Access", "Поўны доступ"), ("Screen Share", "Дэманстрацыя экрана"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland патрабуе Ubuntu версіі 21.04 або навейшай."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Для Wayland патрабуецца вышэйшая версія дыстрыбутыву Linux. Карыстайцеся працоўным сталом X11 або зменіце сваю АС."), + ("ubuntu-21-04-required", "Wayland патрабуе Ubuntu версіі 21.04 або навейшай."), + ("wayland-requires-higher-linux-version", "Для Wayland патрабуецца вышэйшая версія дыстрыбутыву Linux. Карыстайцеся працоўным сталом X11 або зменіце сваю АС."), + ("xdp-portal-unavailable", ""), ("JumpLink", "Перайсці па спасылцы"), ("Please Select the screen to be shared(Operate on the peer side).", "Выберыце экран для дэманстрацыі (кіруецца аддаленай стараной)."), ("Show RustDesk", "Паказаць RustDesk"), diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 3036e31b2..e87322b8b 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Настройки на клавиатурата"), ("Full Access", "Пълен достъп"), ("Screen Share", "Споделяне на екрана"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland изисква Ubuntu 21.04 или по-нов"), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland изисква по-нов Linux. Моля, опитайте с X11 или сменете операционната система."), + ("ubuntu-21-04-required", "Wayland изисква Ubuntu 21.04 или по-нов"), + ("wayland-requires-higher-linux-version", "Wayland изисква по-нов Linux. Моля, опитайте с X11 или сменете операционната система."), + ("xdp-portal-unavailable", ""), ("JumpLink", "Препратка"), ("Please Select the screen to be shared(Operate on the peer side).", "Моля, изберете екрана, който да бъде споделен (спрямо отдалечената страна)."), ("Show RustDesk", "Покажи RustDesk"), diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 05a7e7899..fd78c3ae6 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Configuració del teclat"), ("Full Access", "Accés complet"), ("Screen Share", "Compartició de pantalla"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requereix Ubuntu 21.04 o superior"), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland requereix una versió superior de sistema Linux per a funcionar. Proveu iniciant un entorn d'escriptori amb x11 o actualitzeu el vostre sistema operatiu."), + ("ubuntu-21-04-required", "Wayland requereix Ubuntu 21.04 o superior"), + ("wayland-requires-higher-linux-version", "Wayland requereix una versió superior de sistema Linux per a funcionar. Proveu iniciant un entorn d'escriptori amb x11 o actualitzeu el vostre sistema operatiu."), + ("xdp-portal-unavailable", ""), ("JumpLink", "Marcador"), ("Please Select the screen to be shared(Operate on the peer side).", "Seleccioneu la pantalla que compartireu (quina serà visible al client)"), ("Show RustDesk", "Mostra el RustDesk"), diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 0cc6aacd1..b4026bdf9 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "键盘设置"), ("Full Access", "完全访问"), ("Screen Share", "仅共享屏幕"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland 需要 Ubuntu 21.04 或更高版本。"), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland 需要更高版本的 linux 发行版。 请尝试 X11 桌面或更改您的操作系统。"), + ("ubuntu-21-04-required", "Wayland 需要 Ubuntu 21.04 或更高版本。"), + ("wayland-requires-higher-linux-version", "Wayland 需要更高版本的 linux 发行版。 请尝试 X11 桌面或更改您的操作系统。"), + ("xdp-portal-unavailable", ""), ("JumpLink", "查看"), ("Please Select the screen to be shared(Operate on the peer side).", "请选择要分享的画面(对端操作)。"), ("Show RustDesk", "显示 RustDesk"), diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 944ee4b95..952a55b6c 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Nastavení klávesnice"), ("Full Access", "Úplný přístup"), ("Screen Share", "Sdílení obrazovky"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland vyžaduje Ubuntu 21.04, nebo vyšší verzi."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland vyžaduje vyšší verzi linuxové distribuce. Zkuste prosím X11 desktop, nebo změňte OS."), + ("ubuntu-21-04-required", "Wayland vyžaduje Ubuntu 21.04, nebo vyšší verzi."), + ("wayland-requires-higher-linux-version", "Wayland vyžaduje vyšší verzi linuxové distribuce. Zkuste prosím X11 desktop, nebo změňte OS."), + ("xdp-portal-unavailable", ""), ("JumpLink", "JumpLink"), ("Please Select the screen to be shared(Operate on the peer side).", "Vyberte prosím obrazovku, kterou chcete sdílet (Ovládejte na straně protistrany)."), ("Show RustDesk", "Zobrazit RustDesk"), diff --git a/src/lang/da.rs b/src/lang/da.rs index 8140fcaec..d309fff3f 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Tastaturindstillinger"), ("Full Access", "Fuld adgang"), ("Screen Share", "Skærmdeling"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland kræver Ubuntu version 21.04 eller nyere."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland kræver en højere version af Linux distro. Prøv venligst X11 desktop eller skift dit OS."), + ("ubuntu-21-04-required", "Wayland kræver Ubuntu version 21.04 eller nyere."), + ("wayland-requires-higher-linux-version", "Wayland kræver en højere version af Linux distro. Prøv venligst X11 desktop eller skift dit OS."), + ("xdp-portal-unavailable", ""), ("JumpLink", "JumpLink"), ("Please Select the screen to be shared(Operate on the peer side).", "Vælg venligst den skærm, der skal deles (Betjen på modtagersiden)."), ("Show RustDesk", "Vis RustDesk"), diff --git a/src/lang/de.rs b/src/lang/de.rs index 03e501848..206cb8595 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Tastatureinstellungen"), ("Full Access", "Vollzugriff"), ("Screen Share", "Bildschirmfreigabe"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland erfordert Ubuntu 21.04 oder eine höhere Version."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland erfordert eine höhere Version der Linux-Distribution. Bitte versuchen Sie den X11-Desktop oder ändern Sie Ihr Betriebssystem."), + ("ubuntu-21-04-required", "Wayland erfordert Ubuntu 21.04 oder eine höhere Version."), + ("wayland-requires-higher-linux-version", "Wayland erfordert eine höhere Version der Linux-Distribution. Bitte versuchen Sie den X11-Desktop oder ändern Sie Ihr Betriebssystem."), + ("xdp-portal-unavailable", ""), ("JumpLink", "View"), ("Please Select the screen to be shared(Operate on the peer side).", "Bitte wählen Sie den freizugebenden Bildschirm aus (Bedienung auf der Gegenseite)."), ("Show RustDesk", "RustDesk anzeigen"), diff --git a/src/lang/el.rs b/src/lang/el.rs index 8812f7d04..ab5c6dfa7 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Ρυθμίσεις πληκτρολογίου"), ("Full Access", "Πλήρης πρόσβαση"), ("Screen Share", "Κοινή χρήση οθόνης"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Το Wayland απαιτεί Ubuntu 21.04 ή νεότερη έκδοση."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Το Wayland απαιτεί υψηλότερη έκδοση διανομής του linux. Δοκιμάστε την επιφάνεια εργασίας X11 ή αλλάξτε το λειτουργικό σας σύστημα."), + ("ubuntu-21-04-required", "Το Wayland απαιτεί Ubuntu 21.04 ή νεότερη έκδοση."), + ("wayland-requires-higher-linux-version", "Το Wayland απαιτεί υψηλότερη έκδοση διανομής του linux. Δοκιμάστε την επιφάνεια εργασίας X11 ή αλλάξτε το λειτουργικό σας σύστημα."), + ("xdp-portal-unavailable", ""), ("JumpLink", "Σύνδεσμος μετάβασης"), ("Please Select the screen to be shared(Operate on the peer side).", "Επιλέξτε την οθόνη που θέλετε να μοιραστείτε (Λειτουργία στην πλευρά του απομακρυσμένου σταθμού)."), ("Show RustDesk", "Εμφάνιση του RustDesk"), diff --git a/src/lang/en.rs b/src/lang/en.rs index 511ddff4a..d8190bde0 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -120,6 +120,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Keyboard settings"), ("Full Access", "Full access"), ("Screen Share", "Screen share"), + ("ubuntu-21-04-required", "Wayland requires Ubuntu 21.04 or higher version."), + ("wayland-requires-higher-linux-version", "Wayland requires higher version of linux distro. Please try X11 desktop or change your OS."), + ("xdp-portal-unavailable", "Wayland screen capture failed. The XDG Desktop Portal may have crashed or is unavailable. Try restarting it with `systemctl --user restart xdg-desktop-portal`."), ("JumpLink", "View"), ("Please Select the screen to be shared(Operate on the peer side).", "Please select the screen to be shared(Operate on the peer side)."), ("One-time Password", "One-time password"), diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 3d6b6924f..be7d8a751 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", ""), ("Full Access", ""), ("Screen Share", ""), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland postulas Ubuntu 21.04 aŭ pli altan version."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland postulas pli altan version de linuksa distro. Bonvolu provi X11-labortablon aŭ ŝanĝi vian OS."), + ("ubuntu-21-04-required", "Wayland postulas Ubuntu 21.04 aŭ pli altan version."), + ("wayland-requires-higher-linux-version", "Wayland postulas pli altan version de linuksa distro. Bonvolu provi X11-labortablon aŭ ŝanĝi vian OS."), + ("xdp-portal-unavailable", ""), ("JumpLink", "View"), ("Please Select the screen to be shared(Operate on the peer side).", "Bonvolu Elekti la ekranon por esti dividita (Funkciu ĉe la sama flanko)."), ("Show RustDesk", ""), diff --git a/src/lang/es.rs b/src/lang/es.rs index 8ad0c4cab..524c9a98e 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Ajustes de teclado"), ("Full Access", "Acceso completo"), ("Screen Share", "Compartir pantalla"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requiere Ubuntu 21.04 o una versión superior."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland requiere una versión superior de la distribución de Linux. Pruebe el escritorio X11 o cambie su sistema operativo."), + ("ubuntu-21-04-required", "Wayland requiere Ubuntu 21.04 o una versión superior."), + ("wayland-requires-higher-linux-version", "Wayland requiere una versión superior de la distribución de Linux. Pruebe el escritorio X11 o cambie su sistema operativo."), + ("xdp-portal-unavailable", ""), ("JumpLink", "Ver"), ("Please Select the screen to be shared(Operate on the peer side).", "Seleccione la pantalla que se compartirá (Operar en el lado del par)."), ("Show RustDesk", "Mostrar RustDesk"), diff --git a/src/lang/et.rs b/src/lang/et.rs index def665ec5..3a90a1bd7 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Klaviatuurisätted"), ("Full Access", "Täielik ligipääs"), ("Screen Share", "Ekraanijagamine"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland nõuab Ubuntu 21.04 või uuemat versiooni."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland nõuab Linuxi distributsiooni uuemat versiooni. Palun proovi X11 töölaual või muuda oma operatsioonisüsteemi."), + ("ubuntu-21-04-required", "Wayland nõuab Ubuntu 21.04 või uuemat versiooni."), + ("wayland-requires-higher-linux-version", "Wayland nõuab Linuxi distributsiooni uuemat versiooni. Palun proovi X11 töölaual või muuda oma operatsioonisüsteemi."), + ("xdp-portal-unavailable", ""), ("JumpLink", "JumpLink"), ("Please Select the screen to be shared(Operate on the peer side).", "Palun vali jagatav ekraan (tegutse partneri poolel)."), ("Show RustDesk", "Kuva RustDesk"), diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 2454dcb8a..04bed674a 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Teklatuaren ezarpenak"), ("Full Access", "Sarbide osoa"), ("Screen Share", "Pantailaren partekatzea"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland Ubuntu 21.04 edo bertsio berriagoa behar du."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland-ek linux banaketa berriago bat behar du. Saiatu X11 mahaigainarekin edo aldatu zure sistema eragilea."), + ("ubuntu-21-04-required", "Wayland Ubuntu 21.04 edo bertsio berriagoa behar du."), + ("wayland-requires-higher-linux-version", "Wayland-ek linux banaketa berriago bat behar du. Saiatu X11 mahaigainarekin edo aldatu zure sistema eragilea."), + ("xdp-portal-unavailable", ""), ("JumpLink", "Ikusi"), ("Please Select the screen to be shared(Operate on the peer side).", "Mesedez, hautatu partekatuko den pantaila (Kudeatu parekidearen aldean)"), ("Show RustDesk", "Erakutsi RustDesk"), diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 52be56c81..6de3960a9 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "تنظیمات صفحه کلید"), ("Full Access", "دسترسی کامل"), ("Screen Share", "اشتراک گذاری صفحه"), - ("Wayland requires Ubuntu 21.04 or higher version.", "نیازمند اوبونتو نسخه 21.04 یا بالاتر است Wayland"), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "استفاده کنید و یا سیستم عامل خود را تغییر دهید X11 نیازمند نسخه بالاتری از توزیع لینوکس است. لطفا از دسکتاپ با سیستم"), + ("ubuntu-21-04-required", "نیازمند اوبونتو نسخه 21.04 یا بالاتر است Wayland"), + ("wayland-requires-higher-linux-version", "استفاده کنید و یا سیستم عامل خود را تغییر دهید X11 نیازمند نسخه بالاتری از توزیع لینوکس است. لطفا از دسکتاپ با سیستم"), + ("xdp-portal-unavailable", ""), ("JumpLink", "چشم انداز"), ("Please Select the screen to be shared(Operate on the peer side).", "لطفاً صفحه‌ای را برای اشتراک‌گذاری انتخاب کنید (در سمت همتا به همتا کار کنید)."), ("Show RustDesk", "RustDesk نمایش"), diff --git a/src/lang/fi.rs b/src/lang/fi.rs index 0d9b42ddd..3dc01b4d5 100644 --- a/src/lang/fi.rs +++ b/src/lang/fi.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Näppäimistöasetukset"), ("Full Access", "Täysi käyttöoikeus"), ("Screen Share", "Näytönjako"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland vaatii Ubuntu 21.04:n tai uudemman version."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland vaatii uudemman Linux jakelun version. Kokeile X11 työpöytää tai vaihda käyttöjärjestelmää."), + ("ubuntu-21-04-required", "Wayland vaatii Ubuntu 21.04:n tai uudemman version."), + ("wayland-requires-higher-linux-version", "Wayland vaatii uudemman Linux jakelun version. Kokeile X11 työpöytää tai vaihda käyttöjärjestelmää."), + ("xdp-portal-unavailable", ""), ("JumpLink", "Pikalinkki"), ("Please Select the screen to be shared(Operate on the peer side).", "Valitse jaettava näyttö (toiminto etäpäässä)."), ("Show RustDesk", "Näytä RustDesk"), diff --git a/src/lang/fr.rs b/src/lang/fr.rs index fed35727e..bf10c7fff 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Paramètres du clavier"), ("Full Access", "Accès total"), ("Screen Share", "Partage d’écran"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland nécessite Ubuntu 21.04 ou une version ultérieure."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland nécessite une version ultérieure de votre distribution Linux. Veuillez essayer le bureau X11 ou changer de système d’exploitation."), + ("ubuntu-21-04-required", "Wayland nécessite Ubuntu 21.04 ou une version ultérieure."), + ("wayland-requires-higher-linux-version", "Wayland nécessite une version ultérieure de votre distribution Linux. Veuillez essayer le bureau X11 ou changer de système d’exploitation."), + ("xdp-portal-unavailable", ""), ("JumpLink", "Afficher"), ("Please Select the screen to be shared(Operate on the peer side).", "Veuillez sélectionner l’écran à partager (côté appareil distant)."), ("Show RustDesk", "Afficher RustDesk"), diff --git a/src/lang/ge.rs b/src/lang/ge.rs index 10b5e7f27..8afb46704 100644 --- a/src/lang/ge.rs +++ b/src/lang/ge.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "კლავიატურის პარამეტრები"), ("Full Access", "სრული წვდომა"), ("Screen Share", "ეკრანის გაზიარება"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland საჭიროებს Ubuntu 21.04 ან უფრო ახალ ვერსიას."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland-ს სჭირდება Linux-ის დისტრიბუტივის უფრო ახალი ვერსია. გამოიყენეთ X11 სამუშაო მაგიდა ან შეცვალეთ ოპერაციული სისტემა."), + ("ubuntu-21-04-required", "Wayland საჭიროებს Ubuntu 21.04 ან უფრო ახალ ვერსიას."), + ("wayland-requires-higher-linux-version", "Wayland-ს სჭირდება Linux-ის დისტრიბუტივის უფრო ახალი ვერსია. გამოიყენეთ X11 სამუშაო მაგიდა ან შეცვალეთ ოპერაციული სისტემა."), + ("xdp-portal-unavailable", ""), ("JumpLink", "ნახვა"), ("Please Select the screen to be shared(Operate on the peer side).", "აირჩიეთ ეკრანი გასაზიარებლად (იმუშავეთ პარტნიორის მხარეს)."), ("Show RustDesk", "RustDesk-ის ჩვენება"), diff --git a/src/lang/he.rs b/src/lang/he.rs index 00999708f..1e2d84b71 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "הגדרות מקלדת"), ("Full Access", "גישה מלאה"), ("Screen Share", "שיתוף מסך"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland דורש Ubuntu 21.04 או גרסה גבוהה יותר"), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland דורש גרסת הפצת לינוקס גבוהה יותר. אנא נסה שולחן עבודה מסוג X11 או החלף מערכת הפעלה"), + ("ubuntu-21-04-required", "Wayland דורש Ubuntu 21.04 או גרסה גבוהה יותר"), + ("wayland-requires-higher-linux-version", "Wayland דורש גרסת הפצת לינוקס גבוהה יותר. אנא נסה שולחן עבודה מסוג X11 או החלף מערכת הפעלה"), + ("xdp-portal-unavailable", ""), ("JumpLink", "קישור מהיר"), ("Please Select the screen to be shared(Operate on the peer side).", "אנא בחר את המסך לשיתוף (פעולה בצד העמית)."), ("Show RustDesk", "הצג את RustDesk"), diff --git a/src/lang/hr.rs b/src/lang/hr.rs index d00fc56b9..8ae5d2d96 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Postavke tipkovnice"), ("Full Access", "Potpuni pristup"), ("Screen Share", "Dijeljenje zaslona"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland zahtijeva Ubuntu verziju 21.04 ili višu"), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland zahtijeva višu verziju Linux distribucije. Molimo isprobjate X11 ili promijenite OS."), + ("ubuntu-21-04-required", "Wayland zahtijeva Ubuntu verziju 21.04 ili višu"), + ("wayland-requires-higher-linux-version", "Wayland zahtijeva višu verziju Linux distribucije. Molimo isprobjate X11 ili promijenite OS."), + ("xdp-portal-unavailable", ""), ("JumpLink", "Vidi"), ("Please Select the screen to be shared(Operate on the peer side).", "Molimo odaberite zaslon koji će biti podijeljen (Za rad na strani klijenta)"), ("Show RustDesk", "Prikaži RustDesk"), diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 85153f618..5486d16b4 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Billentyűzetbeállítások"), ("Full Access", "Teljes hozzáférés"), ("Screen Share", "Képernyőmegosztás"), - ("Wayland requires Ubuntu 21.04 or higher version.", "A Waylandhez Ubuntu 21.04 vagy újabb verzió szükséges."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "A Wayland a Linux disztribúció magasabb verzióját igényli. Próbálja ki az X11 asztali környezetet, vagy változtassa meg az operációs rendszert."), + ("ubuntu-21-04-required", "A Waylandhez Ubuntu 21.04 vagy újabb verzió szükséges."), + ("wayland-requires-higher-linux-version", "A Wayland a Linux disztribúció magasabb verzióját igényli. Próbálja ki az X11 asztali környezetet, vagy változtassa meg az operációs rendszert."), + ("xdp-portal-unavailable", ""), ("JumpLink", "Hiperhivatkozás"), ("Please Select the screen to be shared(Operate on the peer side).", "Válassza ki a megosztani kívánt képernyőt."), ("Show RustDesk", "A RustDesk megjelenítése"), diff --git a/src/lang/id.rs b/src/lang/id.rs index f898c8bc4..a19d9ad85 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Pengaturan Papan Ketik"), ("Full Access", "Akses penuh"), ("Screen Share", "Berbagi Layar"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland membutuhkan Ubuntu 21.04 atau versi yang lebih tinggi."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland membutuhkan versi distro linux yang lebih tinggi. Silakan coba desktop X11 atau ubah OS Anda."), + ("ubuntu-21-04-required", "Wayland membutuhkan Ubuntu 21.04 atau versi yang lebih tinggi."), + ("wayland-requires-higher-linux-version", "Wayland membutuhkan versi distro linux yang lebih tinggi. Silakan coba desktop X11 atau ubah OS Anda."), + ("xdp-portal-unavailable", ""), ("JumpLink", "Tautan Cepat"), ("Please Select the screen to be shared(Operate on the peer side).", "Silakan Pilih layar yang akan dibagikan kepada rekan anda."), ("Show RustDesk", "Tampilkan RustDesk"), diff --git a/src/lang/it.rs b/src/lang/it.rs index aac87109d..731db3e0b 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Impostazioni tastiera"), ("Full Access", "Accesso completo"), ("Screen Share", "Condivisione schermo"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland richiede Ubuntu 21.04 o versione successiva."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland richiede una versione superiore della distribuzione Linux.\nProva X11 desktop o cambia il sistema operativo."), + ("ubuntu-21-04-required", "Wayland richiede Ubuntu 21.04 o versione successiva."), + ("wayland-requires-higher-linux-version", "Wayland richiede una versione superiore della distribuzione Linux.\nProva X11 desktop o cambia il sistema operativo."), + ("xdp-portal-unavailable", ""), ("JumpLink", "Vai a"), ("Please Select the screen to be shared(Operate on the peer side).", "Seleziona lo schermo da condividere (opera sul lato dispositivo remoto)."), ("Show RustDesk", "Visualizza RustDesk"), diff --git a/src/lang/ja.rs b/src/lang/ja.rs index e033de3b3..c933c8018 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "キーボードの設定"), ("Full Access", "フルアクセス"), ("Screen Share", "画面共有"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland を使用するには、Ubuntu 21.04 以降のバージョンが必要です。"), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland を使用するには、より新しい Linux ディストリビューションが必要です。 X11 デスクトップを試すか、OS を変更してください。"), + ("ubuntu-21-04-required", "Wayland を使用するには、Ubuntu 21.04 以降のバージョンが必要です。"), + ("wayland-requires-higher-linux-version", "Wayland を使用するには、より新しい Linux ディストリビューションが必要です。 X11 デスクトップを試すか、OS を変更してください。"), + ("xdp-portal-unavailable", ""), ("JumpLink", "表示"), ("Please Select the screen to be shared(Operate on the peer side).", "共有する画面を選択してください(リモートコンピューターが操作します)"), ("Show RustDesk", "RustDesk を表示"), diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 7230d1a1f..15cfd10ef 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "키보드 설정"), ("Full Access", "전체 액세스"), ("Screen Share", "화면 공유"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland는 Ubuntu 21.04 이상 버전이 필요합니다."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland는 상위 버전의 Linux 배포판이 필요합니다. X11 데스크탑을 사용하거나 OS를 변경하세요."), + ("ubuntu-21-04-required", "Wayland는 Ubuntu 21.04 이상 버전이 필요합니다."), + ("wayland-requires-higher-linux-version", "Wayland는 상위 버전의 Linux 배포판이 필요합니다. X11 데스크탑을 사용하거나 OS를 변경하세요."), + ("xdp-portal-unavailable", ""), ("JumpLink", "점프 링크"), ("Please Select the screen to be shared(Operate on the peer side).", "공유할 화면을 선택하세요 (피어 측에서 작동)"), ("Show RustDesk", "RustDesk 표시"), diff --git a/src/lang/kz.rs b/src/lang/kz.rs index c3715672d..e3d31cde6 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", ""), ("Full Access", ""), ("Screen Share", ""), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland Ubuntu 21.04 немесе одан жоғары нұсқасын қажет етеді."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland linux дистрибутивінің жоғарырақ нұсқасын қажет етеді. X11 жұмыс үстелін қолданып көріңіз немесе операциялық жүйеңізді өзгертіңіз."), + ("ubuntu-21-04-required", "Wayland Ubuntu 21.04 немесе одан жоғары нұсқасын қажет етеді."), + ("wayland-requires-higher-linux-version", "Wayland linux дистрибутивінің жоғарырақ нұсқасын қажет етеді. X11 жұмыс үстелін қолданып көріңіз немесе операциялық жүйеңізді өзгертіңіз."), + ("xdp-portal-unavailable", ""), ("JumpLink", "View"), ("Please Select the screen to be shared(Operate on the peer side).", "Бөлісетін экранды таңдаңыз (бірдей жағынан жұмыс жасаңыз)."), ("Show RustDesk", ""), diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 91c76291a..28451dc6f 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Klaviatūros nustatymai"), ("Full Access", "Pilna prieiga"), ("Screen Share", "Ekrano bendrinimas"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland reikalauja Ubuntu 21.04 arba naujesnės versijos."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland reikalinga naujesnės Linux Distro versijos. Išbandykite X11 darbalaukį arba pakeiskite OS."), + ("ubuntu-21-04-required", "Wayland reikalauja Ubuntu 21.04 arba naujesnės versijos."), + ("wayland-requires-higher-linux-version", "Wayland reikalinga naujesnės Linux Distro versijos. Išbandykite X11 darbalaukį arba pakeiskite OS."), + ("xdp-portal-unavailable", ""), ("JumpLink", "Peržiūra"), ("Please Select the screen to be shared(Operate on the peer side).", "Prašome pasirinkti ekraną, kurį norite bendrinti (veikiantį kitoje pusėje)."), ("Show RustDesk", "Rodyti RustDesk"), diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 0c8ba694e..9c03bf8fc 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Tastatūras iestatījumi"), ("Full Access", "Pilna piekļuve"), ("Screen Share", "Ekrāna kopīgošana"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland nepieciešama Ubuntu 21.04 vai jaunāka versija."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland nepieciešama augstāka Linux distro versija. Lūdzu, izmēģiniet X11 desktop vai mainiet savu OS."), + ("ubuntu-21-04-required", "Wayland nepieciešama Ubuntu 21.04 vai jaunāka versija."), + ("wayland-requires-higher-linux-version", "Wayland nepieciešama augstāka Linux distro versija. Lūdzu, izmēģiniet X11 desktop vai mainiet savu OS."), + ("xdp-portal-unavailable", ""), ("JumpLink", "Skatīt"), ("Please Select the screen to be shared(Operate on the peer side).", "Lūdzu, atlasiet kopīgojamo ekrānu (darbojieties sesijas pusē)."), ("Show RustDesk", "Rādīt RustDesk"), diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 9c38fcbb8..daf1c90d2 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Tastaturinnstillinger"), ("Full Access", "Full tilgang"), ("Screen Share", "Skjermdeling"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland krever Ubuntu version 21.04 eller nyere."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland krever en nyere versjon av Linux. Prøv X11 desktop eller skift OS."), + ("ubuntu-21-04-required", "Wayland krever Ubuntu version 21.04 eller nyere."), + ("wayland-requires-higher-linux-version", "Wayland krever en nyere versjon av Linux. Prøv X11 desktop eller skift OS."), + ("xdp-portal-unavailable", ""), ("JumpLink", "JumpLink"), ("Please Select the screen to be shared(Operate on the peer side).", "vennligst velg den skjermen, som skal deles (fjernstyres)."), ("Show RustDesk", "Vis RustDesk"), diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 99b859248..e999e99ff 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Toetsenbordinstellingen"), ("Full Access", "Volledige Toegang"), ("Screen Share", "Scherm Delen"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland vereist Ubuntu 21.04 of hoger."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland vereist een hogere versie van Linux distro. Probeer X11 desktop of verander van OS."), + ("ubuntu-21-04-required", "Wayland vereist Ubuntu 21.04 of hoger."), + ("wayland-requires-higher-linux-version", "Wayland vereist een hogere versie van Linux distro. Probeer X11 desktop of verander van OS."), + ("xdp-portal-unavailable", ""), ("JumpLink", "JumpLink"), ("Please Select the screen to be shared(Operate on the peer side).", "Selecteer het scherm dat moet worden gedeeld (Bediening aan de kant van de peer)."), ("Show RustDesk", "Toon RustDesk"), diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 000c05921..96cf22d22 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Ustawienia klawiatury"), ("Full Access", "Pełny dostęp"), ("Screen Share", "Udostępnianie ekranu"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland wymaga Ubuntu 21.04 lub nowszego."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland wymaga nowszej dystrybucji Linuksa. Wypróbuj pulpit X11 lub zmień system operacyjny."), + ("ubuntu-21-04-required", "Wayland wymaga Ubuntu 21.04 lub nowszego."), + ("wayland-requires-higher-linux-version", "Wayland wymaga nowszej dystrybucji Linuksa. Wypróbuj pulpit X11 lub zmień system operacyjny."), + ("xdp-portal-unavailable", ""), ("JumpLink", "Podgląd"), ("Please Select the screen to be shared(Operate on the peer side).", "Wybierz ekran do udostępnienia (działaj po zdalnego urządzenia)."), ("Show RustDesk", "Pokaż RustDesk"), diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index ccbdd574e..8637ce63c 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Configurações do teclado"), ("Full Access", "Controlo total"), ("Screen Share", ""), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requer Ubuntu 21.04 ou versão superior."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland requer uma versão superior da distribuição linux. Por favor, tente o desktop X11 ou mude seu sistema operacional."), + ("ubuntu-21-04-required", "Wayland requer Ubuntu 21.04 ou versão superior."), + ("wayland-requires-higher-linux-version", "Wayland requer uma versão superior da distribuição linux. Por favor, tente o desktop X11 ou mude seu sistema operacional."), + ("xdp-portal-unavailable", ""), ("JumpLink", "View"), ("Please Select the screen to be shared(Operate on the peer side).", "Por favor, selecione a tela a ser compartilhada (operar no lado do peer)."), ("Show RustDesk", ""), diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index a7a2f7db6..b642cd75a 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Configurações de teclado"), ("Full Access", "Acesso completo"), ("Screen Share", "Compartilhamento de tela"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requer Ubuntu 21.04 ou versão superior."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland requer uma versão superior da distribuição linux. Por favor, tente o desktop X11 ou mude seu sistema operacional."), + ("ubuntu-21-04-required", "Wayland requer Ubuntu 21.04 ou versão superior."), + ("wayland-requires-higher-linux-version", "Wayland requer uma versão superior da distribuição linux. Por favor, tente o desktop X11 ou mude seu sistema operacional."), + ("xdp-portal-unavailable", ""), ("JumpLink", "JumpLink"), ("Please Select the screen to be shared(Operate on the peer side).", "Por favor, selecione a tela a ser compartilhada (operar no lado do parceiro)."), ("Show RustDesk", "Exibir RustDesk"), diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 8917b2a46..69f72f316 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Setări tastatură"), ("Full Access", "Acces total"), ("Screen Share", "Partajare ecran"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland necesită Ubuntu 21.04 sau o versiune superioară."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland necesită o versiune superioară a distribuției Linux. Încearcă desktopul X11 sau schimbă sistemul de operare."), + ("ubuntu-21-04-required", "Wayland necesită Ubuntu 21.04 sau o versiune superioară."), + ("wayland-requires-higher-linux-version", "Wayland necesită o versiune superioară a distribuției Linux. Încearcă desktopul X11 sau schimbă sistemul de operare."), + ("xdp-portal-unavailable", ""), ("JumpLink", "Afișează"), ("Please Select the screen to be shared(Operate on the peer side).", "Partajează ecranul care urmează să fie partajat (operează din partea dispozitivului pereche)."), ("Show RustDesk", "Afișează RustDesk"), diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 35114efe3..8da64748a 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Настройки клавиатуры"), ("Full Access", "Полный доступ"), ("Screen Share", "Демонстрация экрана"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland требуется Ubuntu версии 21.04 или новее."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Для Wayland требуется более поздняя версия дистрибутива Linux. Используйте рабочий стол X11 или смените ОС."), + ("ubuntu-21-04-required", "Wayland требуется Ubuntu версии 21.04 или новее."), + ("wayland-requires-higher-linux-version", "Для Wayland требуется более поздняя версия дистрибутива Linux. Используйте рабочий стол X11 или смените ОС."), + ("xdp-portal-unavailable", ""), ("JumpLink", "Просмотр"), ("Please Select the screen to be shared(Operate on the peer side).", "Выберите экран для демонстрации (работайте на одноранговой стороне)."), ("Show RustDesk", "Показать RustDesk"), diff --git a/src/lang/sc.rs b/src/lang/sc.rs index 2eef86908..c67d88f99 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Impostatziones de tecladu"), ("Full Access", "Atzessu cumpridu"), ("Screen Share", "Cumpartzidura de ischermu"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland tenet bisòngiu de Ubuntu 21.04 o versione prus noa."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland tenet bisòngiu de una versione prus noa de sa distributzione Linux.\nProa X11 pro elaboradores o càmbia su sistema operativu."), + ("ubuntu-21-04-required", "Wayland tenet bisòngiu de Ubuntu 21.04 o versione prus noa."), + ("wayland-requires-higher-linux-version", "Wayland tenet bisòngiu de una versione prus noa de sa distributzione Linux.\nProa X11 pro elaboradores o càmbia su sistema operativu."), + ("xdp-portal-unavailable", ""), ("JumpLink", "Bae a"), ("Please Select the screen to be shared(Operate on the peer side).", "Seletziona s'ischermu de cumpartzire (òpera dae s'ala de su dispositivu remotu)."), ("Show RustDesk", "Mustra RustDesk"), diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 0b45d7e12..9132485ac 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Nastavenia klávesnice"), ("Full Access", "Úplný prístup"), ("Screen Share", "Zdielanie obrazovky"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland vyžaduje Ubuntu 21.04 alebo vyššiu verziu."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland vyžaduje vyššiu verziu linuxovej distribúcie. Skúste X11 desktop alebo zmeňte OS."), + ("ubuntu-21-04-required", "Wayland vyžaduje Ubuntu 21.04 alebo vyššiu verziu."), + ("wayland-requires-higher-linux-version", "Wayland vyžaduje vyššiu verziu linuxovej distribúcie. Skúste X11 desktop alebo zmeňte OS."), + ("xdp-portal-unavailable", ""), ("JumpLink", "View"), ("Please Select the screen to be shared(Operate on the peer side).", "Vyberte obrazovku, ktorú chcete zdieľať (Ovládajte na strane partnera)."), ("Show RustDesk", "Zobraziť RustDesk"), diff --git a/src/lang/sl.rs b/src/lang/sl.rs index d8e22a3c4..6dfb5d572 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Nastavitve tipkovnice"), ("Full Access", "Poln dostop"), ("Screen Share", "Deljenje zaslona"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland zahteva Ubuntu 21.04 ali novejši"), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Zahtevana je novejša različica Waylanda. Posodobite vašo distribucijo ali pa uporabite X11."), + ("ubuntu-21-04-required", "Wayland zahteva Ubuntu 21.04 ali novejši"), + ("wayland-requires-higher-linux-version", "Zahtevana je novejša različica Waylanda. Posodobite vašo distribucijo ali pa uporabite X11."), + ("xdp-portal-unavailable", ""), ("JumpLink", "Pogled"), ("Please Select the screen to be shared(Operate on the peer side).", "Izberite zaslon za delitev (na oddaljeni strani)."), ("Show RustDesk", "Prikaži RustDesk"), diff --git a/src/lang/sq.rs b/src/lang/sq.rs index b7b7321ab..0cfdb03be 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Cilësimet e tastierës"), ("Full Access", "Qasje e plotë"), ("Screen Share", "Ndarja e ekranit"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland kërkon Ubuntu 21.04 ose version më të lartë"), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland kërkon një version më të lartë të shpërndarjes linux. Ju lutemi provoni desktopin X11 ose ndryshoni OS."), + ("ubuntu-21-04-required", "Wayland kërkon Ubuntu 21.04 ose version më të lartë"), + ("wayland-requires-higher-linux-version", "Wayland kërkon një version më të lartë të shpërndarjes linux. Ju lutemi provoni desktopin X11 ose ndryshoni OS."), + ("xdp-portal-unavailable", ""), ("JumpLink", "JumpLink"), ("Please Select the screen to be shared(Operate on the peer side).", "Ju lutemi zgjidhni ekranin që do të ndahet (Vepro në anën e kolegëve"), ("Show RustDesk", "Shfaq RustDesk"), diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 46cb14cdd..743aacc2c 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Postavke tastature"), ("Full Access", "Pun pristup"), ("Screen Share", "Deljenje ekrana"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland zahteva Ubuntu 21.04 ili veću verziju"), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland zahteva veću verziju Linux distribucije. Molimo pokušajte X11 ili promenite OS."), + ("ubuntu-21-04-required", "Wayland zahteva Ubuntu 21.04 ili veću verziju"), + ("wayland-requires-higher-linux-version", "Wayland zahteva veću verziju Linux distribucije. Molimo pokušajte X11 ili promenite OS."), + ("xdp-portal-unavailable", ""), ("JumpLink", "Vidi"), ("Please Select the screen to be shared(Operate on the peer side).", "Molimo izaberite ekran koji će biti podeljen (Za rad na klijent strani)"), ("Show RustDesk", "Prikazi RustDesk"), diff --git a/src/lang/sv.rs b/src/lang/sv.rs index d2d1a3911..52a451474 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Tangentbordsinställningar"), ("Full Access", "Full tillgång"), ("Screen Share", "Skärmdelning"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland kräver Ubuntu 21.04 eller högre."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland kräver en högre version av linux. Försök igen eller byt OS."), + ("ubuntu-21-04-required", "Wayland kräver Ubuntu 21.04 eller högre."), + ("wayland-requires-higher-linux-version", "Wayland kräver en högre version av linux. Försök igen eller byt OS."), + ("xdp-portal-unavailable", ""), ("JumpLink", "JumpLink"), ("Please Select the screen to be shared(Operate on the peer side).", "Välj skärm att dela"), ("Show RustDesk", "Visa RustDesk"), diff --git a/src/lang/ta.rs b/src/lang/ta.rs index 7e3ae5cd0..e4be24259 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "விசைப்பலகை அமைப்புகள்"), ("Full Access", "முழு அணுகல்"), ("Screen Share", "திரை பகிர்வு"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland க்கு Ubuntu 21.04+ தேவை"), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland க்கு உயர் Linux பதிப்பு தேவை. X11 முயற்சிக்கவும் அல்லது OS மாற்றவும்."), + ("ubuntu-21-04-required", "Wayland க்கு Ubuntu 21.04+ தேவை"), + ("wayland-requires-higher-linux-version", "Wayland க்கு உயர் Linux பதிப்பு தேவை. X11 முயற்சிக்கவும் அல்லது OS மாற்றவும்."), + ("xdp-portal-unavailable", ""), ("JumpLink", "ஜம்ப் லிங்க்"), ("Please Select the screen to be shared(Operate on the peer side).", "பகிரப்பட வேண்டிய திரை தேர்ந்தெடுக்கவும்"), ("Show RustDesk", "RustDesk ஐ காட்டு"), diff --git a/src/lang/template.rs b/src/lang/template.rs index b21f64f14..b70bb616b 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", ""), ("Full Access", ""), ("Screen Share", ""), - ("Wayland requires Ubuntu 21.04 or higher version.", ""), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", ""), + ("ubuntu-21-04-required", ""), + ("wayland-requires-higher-linux-version", ""), + ("xdp-portal-unavailable", ""), ("JumpLink", ""), ("Please Select the screen to be shared(Operate on the peer side).", ""), ("Show RustDesk", ""), diff --git a/src/lang/th.rs b/src/lang/th.rs index dbfc1096c..f4bf65798 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "การตั้งค่าคีย์บอร์ด"), ("Full Access", "การเข้าถึงทั้งหมด"), ("Screen Share", "การแชร์จอ"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland ต้องการ Ubuntu เวอร์ชัน 21.04 หรือสูงกว่า"), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland ต้องการลินุกซ์เวอร์ชันที่สูงกว่านี้ กรุณาเปลี่ยนไปใช้เดสก์ท็อป X11 หรือเปลี่ยนระบบปฏิบัติการของคุณ"), + ("ubuntu-21-04-required", "Wayland ต้องการ Ubuntu เวอร์ชัน 21.04 หรือสูงกว่า"), + ("wayland-requires-higher-linux-version", "Wayland ต้องการลินุกซ์เวอร์ชันที่สูงกว่านี้ กรุณาเปลี่ยนไปใช้เดสก์ท็อป X11 หรือเปลี่ยนระบบปฏิบัติการของคุณ"), + ("xdp-portal-unavailable", ""), ("JumpLink", "View"), ("Please Select the screen to be shared(Operate on the peer side).", "กรุณาเลือกหน้าจอที่ต้องการแชร์ (ใช้งานในอีกฝั่งของการเชื่อมต่อ)"), ("Show RustDesk", "แสดง RustDesk"), diff --git a/src/lang/tr.rs b/src/lang/tr.rs index e70d0a497..3f7c21c2b 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Klavye Ayarları"), ("Full Access", "Tam Erişim"), ("Screen Share", "Ekran Paylaşımı"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland, Ubuntu 21.04 veya daha yüksek bir sürüm gerektirir."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland, linux dağıtımının daha yüksek bir sürümünü gerektirir. Lütfen X11 masaüstünü deneyin veya işletim sisteminizi değiştirin."), + ("ubuntu-21-04-required", "Wayland, Ubuntu 21.04 veya daha yüksek bir sürüm gerektirir."), + ("wayland-requires-higher-linux-version", "Wayland, linux dağıtımının daha yüksek bir sürümünü gerektirir. Lütfen X11 masaüstünü deneyin veya işletim sisteminizi değiştirin."), + ("xdp-portal-unavailable", ""), ("JumpLink", "View"), ("Please Select the screen to be shared(Operate on the peer side).", "Lütfen paylaşılacak ekranı seçiniz (Ekran tarafında çalıştırın)."), ("Show RustDesk", "RustDesk'i Göster"), diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 0e01fcde5..1172fe2cc 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "鍵盤設定"), ("Full Access", "完全存取"), ("Screen Share", "僅分享螢幕畫面"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland 需要 Ubuntu 21.04 或更新的版本。"), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland 需要更新版的 Linux 發行版。請嘗試使用 X11 桌面或更改您的作業系統。"), + ("ubuntu-21-04-required", "Wayland 需要 Ubuntu 21.04 或更新的版本。"), + ("wayland-requires-higher-linux-version", "Wayland 需要更新版的 Linux 發行版。請嘗試使用 X11 桌面或更改您的作業系統。"), + ("xdp-portal-unavailable", ""), ("JumpLink", "查看"), ("Please Select the screen to be shared(Operate on the peer side).", "請選擇要分享的螢幕畫面(在對方的裝置上操作)。"), ("Show RustDesk", "顯示 RustDesk"), diff --git a/src/lang/uk.rs b/src/lang/uk.rs index b49b2e5ae..146b89569 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Налаштування клавіатури"), ("Full Access", "Повний доступ"), ("Screen Share", "Демонстрація екрана"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland потребує Ubuntu 21.04 або новішої версії."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Для Wayland потрібна новіша версія дистрибутива Linux. Будь ласка, спробуйте стільницю на X11 або змініть свою ОС."), + ("ubuntu-21-04-required", "Wayland потребує Ubuntu 21.04 або новішої версії."), + ("wayland-requires-higher-linux-version", "Для Wayland потрібна новіша версія дистрибутива Linux. Будь ласка, спробуйте стільницю на X11 або змініть свою ОС."), + ("xdp-portal-unavailable", ""), ("JumpLink", "Перегляд"), ("Please Select the screen to be shared(Operate on the peer side).", "Будь ласка, виберіть екран, до якого потрібно надати доступ (на віддаленому пристрої)."), ("Show RustDesk", "Показати RustDesk"), diff --git a/src/lang/vi.rs b/src/lang/vi.rs index 8f5888509..6ba287912 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -377,8 +377,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keyboard Settings", "Cài đặt bàn phím"), ("Full Access", "Toàn quyền truy cập"), ("Screen Share", "Chia sẻ màn hình"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland yêu cầu Ubuntu 21.04 trở lên."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland yêu cầu phiên bản Linux mới hơn. Hãy thử X11 hoặc đổi hệ điều hành."), + ("ubuntu-21-04-required", "Wayland yêu cầu Ubuntu 21.04 trở lên."), + ("wayland-requires-higher-linux-version", "Wayland yêu cầu phiên bản Linux mới hơn. Hãy thử X11 hoặc đổi hệ điều hành."), + ("xdp-portal-unavailable", ""), ("JumpLink", "Xem"), ("Please Select the screen to be shared(Operate on the peer side).", "Vui lòng chọn màn hình chia sẻ (Thao tác ở phía đối tác)."), ("Show RustDesk", "Hiện RustDesk"), diff --git a/src/server/wayland.rs b/src/server/wayland.rs index 6eb6a97bf..1e0efc0f4 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -10,7 +10,8 @@ use std::io; use crate::{ client::{ - SCRAP_OTHER_VERSION_OR_X11_REQUIRED, SCRAP_UBUNTU_HIGHER_REQUIRED, SCRAP_X11_REQUIRED, + SCRAP_OTHER_VERSION_OR_X11_REQUIRED, SCRAP_UBUNTU_HIGHER_REQUIRED, + SCRAP_X11_REQUIRED, SCRAP_XDP_PORTAL_UNAVAILABLE, }, platform::linux::is_x11, }; @@ -56,10 +57,15 @@ fn map_err_scrap(err: String) -> io::Error { } } else { try_log(&err); - if err.contains("org.freedesktop.portal") - || err.contains("pipewire") - || err.contains("dbus") + let err_lower = err.to_ascii_lowercase(); + if err_lower.contains("org.freedesktop.portal") + || err_lower.contains("dbus") + || err_lower.contains("d-bus") { + // The portal D-Bus interface is unreachable. This typically means + // xdg-desktop-portal has crashed... for more info, see: Issue #12897 + io::Error::new(io::ErrorKind::Other, SCRAP_XDP_PORTAL_UNAVAILABLE) + } else if err_lower.contains("pipewire") { io::Error::new(io::ErrorKind::Other, SCRAP_OTHER_VERSION_OR_X11_REQUIRED) } else { io::Error::new(io::ErrorKind::Other, SCRAP_X11_REQUIRED) From c0da4a6645bbc5eb7c1519b69b2371e641c0b9ec Mon Sep 17 00:00:00 2001 From: Lynilia <89228568+Lynilia@users.noreply.github.com> Date: Wed, 18 Mar 2026 15:53:30 +0100 Subject: [PATCH 181/277] Update fr.rs (#14567) --- src/lang/fr.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/fr.rs b/src/lang/fr.rs index bf10c7fff..56b19a33d 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -379,7 +379,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Share", "Partage d’écran"), ("ubuntu-21-04-required", "Wayland nécessite Ubuntu 21.04 ou une version ultérieure."), ("wayland-requires-higher-linux-version", "Wayland nécessite une version ultérieure de votre distribution Linux. Veuillez essayer le bureau X11 ou changer de système d’exploitation."), - ("xdp-portal-unavailable", ""), + ("xdp-portal-unavailable", "Échec de la capture de l’écran Wayland. Le portail de bureau XDG a peut-être planté ou n’est pas disponible. Essayez de le redémarrer avec la commande `systemctl --user restart xdg-desktop-portal`."), ("JumpLink", "Afficher"), ("Please Select the screen to be shared(Operate on the peer side).", "Veuillez sélectionner l’écran à partager (côté appareil distant)."), ("Show RustDesk", "Afficher RustDesk"), From c457b0e7d3a8ca8ad7a0541c180f791890481b4e Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 19 Mar 2026 20:04:10 +0800 Subject: [PATCH 182/277] add option to hide stop-service when service is running (#14563) * add option to hide stop-service when service is running Signed-off-by: 21pages * update hbb_common to upstream Signed-off-by: 21pages --------- Signed-off-by: 21pages --- .../flutter_hbb/FloatingWindowService.kt | 6 ++- flutter/android/app/src/main/kotlin/ffi.kt | 1 + flutter/lib/consts.dart | 1 + .../desktop/pages/desktop_setting_page.dart | 33 ++++++++++------ flutter/lib/mobile/pages/server_page.dart | 22 ++++++----- libs/hbb_common | 2 +- src/flutter_ffi.rs | 16 ++++++++ src/tray.rs | 39 +++++++++++++------ src/ui/index.tis | 3 +- 9 files changed, 87 insertions(+), 36 deletions(-) diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt index 696d536c6..6dd4a2f61 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt @@ -311,7 +311,10 @@ class FloatingWindowService : Service(), View.OnTouchListener { popupMenu.menu.add(0, idSyncClipboard, 0, translate("Update client clipboard")) } val idStopService = 2 - popupMenu.menu.add(0, idStopService, 0, translate("Stop service")) + val hideStopService = FFI.getBuildinOption("hide-stop-service") == "Y" + if (!hideStopService) { + popupMenu.menu.add(0, idStopService, 0, translate("Stop service")) + } popupMenu.setOnMenuItemClickListener { menuItem -> when (menuItem.itemId) { idShowRustDesk -> { @@ -389,4 +392,3 @@ class FloatingWindowService : Service(), View.OnTouchListener { return false } } - diff --git a/flutter/android/app/src/main/kotlin/ffi.kt b/flutter/android/app/src/main/kotlin/ffi.kt index 8e9b39968..e3c9d9830 100644 --- a/flutter/android/app/src/main/kotlin/ffi.kt +++ b/flutter/android/app/src/main/kotlin/ffi.kt @@ -24,6 +24,7 @@ object FFI { external fun setFrameRawEnable(name: String, value: Boolean) external fun setCodecInfo(info: String) external fun getLocalOption(key: String): String + external fun getBuildinOption(key: String): String external fun onClipboardUpdate(clips: ByteBuffer) external fun isServiceClipboardEnabled(): Boolean } diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 3b9940c9c..b1112dd29 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -175,6 +175,7 @@ const String kOptionEnableFlutterHttpOnRust = "enable-flutter-http-on-rust"; const String kOptionHideServerSetting = "hide-server-settings"; const String kOptionHideProxySetting = "hide-proxy-settings"; const String kOptionHideWebSocketSetting = "hide-websocket-settings"; +const String kOptionHideStopService = "hide-stop-service"; const String kOptionHideRemotePrinterSetting = "hide-remote-printer-settings"; const String kOptionHideSecuritySetting = "hide-security-settings"; const String kOptionHideNetworkSetting = "hide-network-settings"; diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 82212d191..029629b24 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -458,18 +458,27 @@ class _GeneralState extends State<_General> { return const Offstage(); } - return _Card(title: 'Service', children: [ - Obx(() => _Button(serviceStop.value ? 'Start' : 'Stop', () { - () async { - serviceBtnEnabled.value = false; - await start_service(serviceStop.value); - // enable the button after 1 second - Future.delayed(const Duration(seconds: 1), () { - serviceBtnEnabled.value = true; - }); - }(); - }, enabled: serviceBtnEnabled.value)) - ]); + final hideStopService = + bind.mainGetBuildinOption(key: kOptionHideStopService) == 'Y'; + + return Obx(() { + if (hideStopService && !serviceStop.value) { + return const Offstage(); + } + + return _Card(title: 'Service', children: [ + _Button(serviceStop.value ? 'Start' : 'Stop', () { + () async { + serviceBtnEnabled.value = false; + await start_service(serviceStop.value); + // enable the button after 1 second + Future.delayed(const Duration(seconds: 1), () { + serviceBtnEnabled.value = true; + }); + }(); + }, enabled: serviceBtnEnabled.value) + ]); + }); } Widget other() { diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index 54406ff2e..57856a4d7 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -582,10 +582,13 @@ class _PermissionCheckerState extends State { Widget build(BuildContext context) { final serverModel = Provider.of(context); final hasAudioPermission = androidVersion >= 30; + final hideStopService = + isAndroid && + bind.mainGetBuildinOption(key: kOptionHideStopService) == 'Y'; return PaddingCard( title: translate("Permissions"), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - serverModel.mediaOk + serverModel.mediaOk && !hideStopService ? ElevatedButton.icon( style: ButtonStyle( backgroundColor: @@ -595,14 +598,15 @@ class _PermissionCheckerState extends State { label: Text(translate("Stop service"))) .marginOnly(bottom: 8) : SizedBox.shrink(), - PermissionRow( - translate("Screen Capture"), - serverModel.mediaOk, - !serverModel.mediaOk && - gFFI.userModel.userName.value.isEmpty && - bind.mainGetLocalOption(key: "show-scam-warning") != "N" - ? () => showScamWarning(context, serverModel) - : serverModel.toggleService), + if (!hideStopService || !serverModel.mediaOk) + PermissionRow( + translate("Screen Capture"), + serverModel.mediaOk, + !serverModel.mediaOk && + gFFI.userModel.userName.value.isEmpty && + bind.mainGetLocalOption(key: "show-scam-warning") != "N" + ? () => showScamWarning(context, serverModel) + : serverModel.toggleService), PermissionRow(translate("Input Control"), serverModel.inputOk, serverModel.toggleInput), PermissionRow(translate("Transfer file"), serverModel.fileOk, diff --git a/libs/hbb_common b/libs/hbb_common index 48c37de3e..648b63942 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 48c37de3e6c4e399af6f51ca20e8e3e1fd037976 +Subproject commit 648b639427953cb8b052b4d80aeb882c644c4ce9 diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 551ad799f..092e6d295 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -3049,6 +3049,22 @@ pub mod server_side { return env.new_string(res).unwrap_or_default().into_raw(); } + #[no_mangle] + pub unsafe extern "system" fn Java_ffi_FFI_getBuildinOption( + env: JNIEnv, + _class: JClass, + key: JString, + ) -> jstring { + let mut env = env; + let res = if let Ok(key) = env.get_string(&key) { + let key: String = key.into(); + super::get_builtin_option(&key) + } else { + "".into() + }; + return env.new_string(res).unwrap_or_default().into_raw(); + } + #[no_mangle] pub unsafe extern "system" fn Java_ffi_FFI_isServiceClipboardEnabled( env: JNIEnv, diff --git a/src/tray.rs b/src/tray.rs index 8ab4e3ecb..e8db0efc0 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -54,9 +54,22 @@ fn make_tray() -> hbb_common::ResultType<()> { let mut event_loop = EventLoopBuilder::new().build(); let tray_menu = Menu::new(); - let quit_i = MenuItem::new(translate("Stop service".to_owned()), true, None); + let hide_stop_service = crate::ui_interface::get_builtin_option( + hbb_common::config::keys::OPTION_HIDE_STOP_SERVICE, + ) == "Y"; + // The tray icon is only shown when the service is running, so we don't need to check + // the `stop-service` option here. + let quit_i = if !hide_stop_service { + Some(MenuItem::new(translate("Stop service".to_owned()), true, None)) + } else { + None + }; let open_i = MenuItem::new(translate("Open".to_owned()), true, None); - tray_menu.append_items(&[&open_i, &quit_i]).ok(); + if let Some(quit_i) = &quit_i { + tray_menu.append_items(&[&open_i, quit_i]).ok(); + } else { + tray_menu.append_items(&[&open_i]).ok(); + } let tooltip = |count: usize| { if count == 0 { format!( @@ -155,15 +168,19 @@ fn make_tray() -> hbb_common::ResultType<()> { } if let Ok(event) = menu_channel.try_recv() { - if event.id == quit_i.id() { - /* failed in windows, seems no permission to check system process - if !crate::check_process("--server", false) { - *control_flow = ControlFlow::Exit; - return; - } - */ - if !crate::platform::uninstall_service(false, false) { - *control_flow = ControlFlow::Exit; + if let Some(quit_i) = &quit_i { + if event.id == quit_i.id() { + /* failed in windows, seems no permission to check system process + if !crate::check_process("--server", false) { + *control_flow = ControlFlow::Exit; + return; + } + */ + if !crate::platform::uninstall_service(false, false) { + *control_flow = ControlFlow::Exit; + } + } else if event.id == open_i.id() { + open_func(); } } else if event.id == open_i.id() { open_func(); diff --git a/src/ui/index.tis b/src/ui/index.tis index 5853fe3e2..acec6a2b5 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -16,6 +16,7 @@ const disable_ab = handler.is_disable_ab(); const hide_server_settings = handler.get_builtin_option("hide-server-settings") == "Y"; const hide_proxy_settings = handler.get_builtin_option("hide-proxy-settings") == "Y"; const hide_websocket_settings = handler.get_builtin_option("hide-websocket-settings") == "Y"; +const hide_stop_service = handler.get_builtin_option("hide-stop-service") == "Y"; const disable_change_permanent_password = handler.get_builtin_option("disable-change-permanent-password") == "Y"; const disable_change_id = handler.get_builtin_option("disable-change-id") == "Y"; @@ -532,7 +533,7 @@ class MyIdMenu: Reactor.Component { {!disable_settings && !using_public_server && !outgoing_only &&
  • {svg_checkmark}{translate('Disable UDP')}
  • } {!disable_settings && !using_public_server &&
  • {svg_checkmark}{translate('Allow insecure TLS fallback')}
  • }
    -
  • {svg_checkmark}{translate("Enable service")}
  • + {(!hide_stop_service || service_stopped) &&
  • {svg_checkmark}{translate("Enable service")}
  • } {!disable_settings && is_win && handler.is_installed() ? : ""} {!disable_settings && } {!disable_settings && false && handler.using_public_server() &&
  • {svg_checkmark}{translate('Always connect via relay')}
  • } From dba5fea66f6c65c375a5280a4a3b4029f6cca38e Mon Sep 17 00:00:00 2001 From: linsui <36977733+linsui@users.noreply.github.com> Date: Fri, 20 Mar 2026 05:45:35 +0000 Subject: [PATCH 183/277] Fix F-Droid 1.4.6 build (#13601) * build_fdroid.sh: avoid using github api * build_fdroid.sh: Find correct LLVM path for LLVM > 15.x Signed-off-by: Vasyl Gello * build_fdroid.sh: formatting / spelling Signed-off-by: Vasyl Gello --------- Signed-off-by: Vasyl Gello Co-authored-by: Vasyl Gello --- flutter/build_fdroid.sh | 42 +++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/flutter/build_fdroid.sh b/flutter/build_fdroid.sh index ecfb444ef..d50a6a6ce 100755 --- a/flutter/build_fdroid.sh +++ b/flutter/build_fdroid.sh @@ -7,7 +7,7 @@ # 2024, Vasyl Gello # -# The script is invoked by F-Droid builder system ste-by-step. +# The script is invoked by F-Droid builder system step-by-step. # # It accepts the following arguments: # @@ -16,7 +16,6 @@ # - Android architecture to build APK for: armeabi-v7a arm64-v8av x86 x86_64 # - The build step to execute: # -# + sudo-deps: as root, install needed Debian packages into builder VM # + prebuild: patch sources and do other stuff before the build # + build: perform actual build of APK file # @@ -184,13 +183,9 @@ prebuild) fi # Map NDK version to revision - - NDK_VERSION="$(wget \ - -qO- \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - 'https://api.github.com/repos/android/ndk/releases' | - jq -r ".[] | select(.tag_name == \"${NDK_VERSION}\") | .body | match(\"ndkVersion \\\"(.*)\\\"\").captures[0].string")" + NDK_VERSION="$(curl https://gitlab.com/fdroid/android-sdk-transparency-log/-/raw/master/signed/checksums.json | + jq -r ".\"https://dl.google.com/android/repository/android-ndk-${NDK_VERSION}-linux.zip\"[0].\"source.properties\"" | + sed -n -E 's/.*Pkg.Revision = ([0-9.]+).*/\1/p')" if [ -z "${NDK_VERSION}" ]; then echo "ERROR: Can not map Android NDK codename to revision!" >&2 @@ -316,6 +311,18 @@ prebuild) # `FLUTTER_BRIDGE_VERSION` an restore the pubspec later if [ "${FLUTTER_VERSION}" != "${FLUTTER_BRIDGE_VERSION}" ]; then + # Find first libclang.so and set BRIDGE_LLVM_PATH + + BRIDGE_LLVM_PATH="$(find /usr/lib/ -name libclang.so | head -n1)" + + if [ -z "${BRIDGE_LLVM_PATH}" ]; then + echo 'ERROR: Can not find libclang.so for bridge generator!' >&2 + exit 1 + fi + + BRIDGE_LLVM_PATH="$(dirname "${BRIDGE_LLVM_PATH}")" + BRIDGE_LLVM_PATH="$(dirname "${BRIDGE_LLVM_PATH}")" + # Install Flutter bridge version prepare_flutter "${FLUTTER_BRIDGE_VERSION}" "${HOME}/flutter" @@ -344,7 +351,8 @@ prebuild) flutter_rust_bridge_codegen \ --rust-input ./src/flutter_ffi.rs \ - --dart-output ./flutter/lib/generated_bridge.dart + --dart-output ./flutter/lib/generated_bridge.dart \ + --llvm-path "${BRIDGE_LLVM_PATH}" # Add bridge files to save-list @@ -355,13 +363,15 @@ prebuild) git checkout '*' git clean -dffx git reset + + unset BRIDGE_LLVM_PATH fi # Install Flutter version for RustDesk library build prepare_flutter "${FLUTTER_VERSION}" "${HOME}/flutter" - # gms is not in thoes files now, but we still keep the following line for future reference(maybe). + # gms is not in these files now, but we still keep the following line for future reference(maybe). sed \ -i \ @@ -414,13 +424,9 @@ build) .github/workflows/flutter-build.yml)" # Map NDK version to revision - - NDK_VERSION="$(wget \ - -qO- \ - -H "Accept: application/vnd.github+json" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - 'https://api.github.com/repos/android/ndk/releases' | - jq -r ".[] | select(.tag_name == \"${NDK_VERSION}\") | .body | match(\"ndkVersion \\\"(.*)\\\"\").captures[0].string")" + NDK_VERSION="$(curl https://gitlab.com/fdroid/android-sdk-transparency-log/-/raw/master/signed/checksums.json | + jq -r ".\"https://dl.google.com/android/repository/android-ndk-${NDK_VERSION}-linux.zip\"[0].\"source.properties\"" | + sed -n -E 's/.*Pkg.Revision = ([0-9.]+).*/\1/p')" if [ -z "${NDK_VERSION}" ]; then echo "ERROR: Can not map Android NDK codename to revision!" >&2 From 899dd46f5be10d9514bbaf568723c6e4396dbcc4 Mon Sep 17 00:00:00 2001 From: solokot Date: Sat, 21 Mar 2026 11:18:39 +0300 Subject: [PATCH 184/277] Update ru.rs (#14570) --- src/lang/ru.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 8da64748a..c28baf600 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -379,7 +379,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Share", "Демонстрация экрана"), ("ubuntu-21-04-required", "Wayland требуется Ubuntu версии 21.04 или новее."), ("wayland-requires-higher-linux-version", "Для Wayland требуется более поздняя версия дистрибутива Linux. Используйте рабочий стол X11 или смените ОС."), - ("xdp-portal-unavailable", ""), + ("xdp-portal-unavailable", "Невозможно сделать снимок экрана Wayland. Возможно, в XDG Desktop Portal сбой или он недоступен. Попробуйте перезапустить его с помощью `systemctl --user restart xdg-desktop-portal`."), ("JumpLink", "Просмотр"), ("Please Select the screen to be shared(Operate on the peer side).", "Выберите экран для демонстрации (работайте на одноранговой стороне)."), ("Show RustDesk", "Показать RustDesk"), From 7004acae46b66b9befd0c9985691a0305fda1229 Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Sat, 21 Mar 2026 09:18:56 +0100 Subject: [PATCH 185/277] Update de.rs (#14572) --- src/lang/de.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 206cb8595..ff4139559 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -379,8 +379,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Share", "Bildschirmfreigabe"), ("ubuntu-21-04-required", "Wayland erfordert Ubuntu 21.04 oder eine höhere Version."), ("wayland-requires-higher-linux-version", "Wayland erfordert eine höhere Version der Linux-Distribution. Bitte versuchen Sie den X11-Desktop oder ändern Sie Ihr Betriebssystem."), - ("xdp-portal-unavailable", ""), - ("JumpLink", "View"), + ("xdp-portal-unavailable", "Die Bildschirmaufnahme mit Wayland ist fehlgeschlagen. Das XDG-Desktop-Portal ist möglicherweise abgestürzt oder nicht verfügbar. Versuchen Sie, es mit `systemctl --user restart xdg-desktop-portal` neu zu starten."), + ("JumpLink", "Anzeigen"), ("Please Select the screen to be shared(Operate on the peer side).", "Bitte wählen Sie den freizugebenden Bildschirm aus (Bedienung auf der Gegenseite)."), ("Show RustDesk", "RustDesk anzeigen"), ("This PC", "Dieser PC"), From ca4647ddd6b626a41d534bce15a3390f2c8aa946 Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Mon, 23 Mar 2026 06:48:34 +0100 Subject: [PATCH 186/277] Italian language update (#14598) --- src/lang/it.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index 731db3e0b..bc2b98eb6 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -379,7 +379,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Share", "Condivisione schermo"), ("ubuntu-21-04-required", "Wayland richiede Ubuntu 21.04 o versione successiva."), ("wayland-requires-higher-linux-version", "Wayland richiede una versione superiore della distribuzione Linux.\nProva X11 desktop o cambia il sistema operativo."), - ("xdp-portal-unavailable", ""), + ("xdp-portal-unavailable", "Acquisizione dello schermo di Wayland non riuscita. Il portale desktop XDG potrebbe essersi bloccato o non essere disponibile. Prova a riavviarlo con `systemctl --user restart xdg-desktop-portal`."), ("JumpLink", "Vai a"), ("Please Select the screen to be shared(Operate on the peer side).", "Seleziona lo schermo da condividere (opera sul lato dispositivo remoto)."), ("Show RustDesk", "Visualizza RustDesk"), From ad1e5330e92c86ff5699823652b12a453db14add Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 24 Mar 2026 20:39:44 +0800 Subject: [PATCH 187/277] update hbb_common --- libs/hbb_common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/hbb_common b/libs/hbb_common index 648b63942..6fb03d076 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 648b639427953cb8b052b4d80aeb882c644c4ce9 +Subproject commit 6fb03d076eae81e244db72e87474eee149a0fb85 From aab34b23384d5e42f628438ada3da6c88b4d9ca6 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 25 Mar 2026 16:36:35 +0800 Subject: [PATCH 188/277] remove winget --- .github/workflows/winget.yml | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 .github/workflows/winget.yml diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml deleted file mode 100644 index 90a3d4fb3..000000000 --- a/.github/workflows/winget.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Publish to WinGet -on: - release: - types: [released] - workflow_dispatch: -jobs: - publish: - runs-on: ubuntu-latest - steps: - - uses: vedantmgoyal9/winget-releaser@main - with: - identifier: RustDesk.RustDesk - version: "1.4.6" - release-tag: "1.4.6" - token: ${{ secrets.WINGET_TOKEN }} From 285e29d2dc0d54b6565c5b3de269b919895042a5 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 26 Mar 2026 12:08:29 +0800 Subject: [PATCH 189/277] fix(shell): check kv in `update_install_option` (#14564) Signed-off-by: fufesou --- src/platform/windows.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index ee8aa7c6f..b579891f1 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -2029,6 +2029,9 @@ pub fn update_install_option(k: &str, v: &str) -> ResultType<()> { if !is_installed() || !crate::is_server() { return Ok(()); } + if ![REG_NAME_INSTALL_PRINTER].contains(&k) || !["0", "1"].contains(&v) { + return Ok(()); + } let app_name = crate::get_app_name(); let ext = app_name.to_lowercase(); let cmds = From 170516572ea3ce663c3b994bdde287f271cdabae Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 26 Mar 2026 14:49:54 +0800 Subject: [PATCH 190/277] refact(password): Store permanent password as hashed verifier (#14619) * refact(password): Store permanent password as hashed verifier Signed-off-by: fufesou * fix(password): remove unused code Signed-off-by: fufesou * fix(password): mobile, password dialog, width 500 Signed-off-by: fufesou --------- Signed-off-by: fufesou --- flutter/lib/common.dart | 5 +- .../lib/desktop/pages/desktop_home_page.dart | 139 +++++++++++++++--- .../desktop/pages/desktop_setting_page.dart | 5 +- flutter/lib/mobile/pages/server_page.dart | 3 +- flutter/lib/mobile/widgets/dialog.dart | 94 ------------ flutter/lib/models/server_model.dart | 11 -- flutter/lib/web/bridge.dart | 8 +- libs/hbb_common | 2 +- src/flutter_ffi.rs | 33 +++-- src/ipc.rs | 135 +++++++++++++++-- src/lang/ar.rs | 2 + src/lang/be.rs | 2 + src/lang/bg.rs | 2 + src/lang/ca.rs | 2 + src/lang/cn.rs | 2 + src/lang/cs.rs | 2 + src/lang/da.rs | 2 + src/lang/de.rs | 2 + src/lang/el.rs | 2 + src/lang/en.rs | 2 + src/lang/eo.rs | 2 + src/lang/es.rs | 2 + src/lang/et.rs | 2 + src/lang/eu.rs | 2 + src/lang/fa.rs | 2 + src/lang/fi.rs | 2 + src/lang/fr.rs | 2 + src/lang/ge.rs | 2 + src/lang/he.rs | 2 + src/lang/hr.rs | 2 + src/lang/hu.rs | 2 + src/lang/id.rs | 2 + src/lang/it.rs | 2 + src/lang/ja.rs | 2 + src/lang/ko.rs | 2 + src/lang/kz.rs | 2 + src/lang/lt.rs | 2 + src/lang/lv.rs | 2 + src/lang/nb.rs | 2 + src/lang/nl.rs | 2 + src/lang/pl.rs | 2 + src/lang/pt_PT.rs | 2 + src/lang/ptbr.rs | 2 + src/lang/ro.rs | 2 + src/lang/ru.rs | 2 + src/lang/sc.rs | 2 + src/lang/sk.rs | 2 + src/lang/sl.rs | 2 + src/lang/sq.rs | 2 + src/lang/sr.rs | 2 + src/lang/sv.rs | 2 + src/lang/ta.rs | 2 + src/lang/template.rs | 2 + src/lang/th.rs | 2 + src/lang/tr.rs | 2 + src/lang/tw.rs | 2 + src/lang/uk.rs | 2 + src/lang/vi.rs | 2 + src/server/connection.rs | 83 +++++++++-- src/ui.rs | 15 +- src/ui/common.css | 7 +- src/ui/index.tis | 63 +++++++- src/ui/msgbox.tis | 6 +- src/ui_interface.rs | 50 ++++++- 64 files changed, 563 insertions(+), 192 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index af87f980f..ad3bbc9f6 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -2377,8 +2377,9 @@ List? urlLinkToCmdArgs(Uri uri) { final password = uri.path.substring("/".length); if (password.isNotEmpty) { Timer(Duration(seconds: 1), () async { - await bind.mainSetPermanentPassword(password: password); - showToast(translate('Successful')); + final ok = + await bind.mainSetPermanentPasswordWithResult(password: password); + showToast(translate(ok ? 'Successful' : 'Failed')); }); } } diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 339ecddb0..42ec10032 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -908,12 +908,17 @@ class _DesktopHomePageState extends State } void setPasswordDialog({VoidCallback? notEmptyCallback}) async { - final pw = await bind.mainGetPermanentPassword(); - final p0 = TextEditingController(text: pw); - final p1 = TextEditingController(text: pw); + final p0 = TextEditingController(text: ""); + final p1 = TextEditingController(text: ""); var errMsg0 = ""; var errMsg1 = ""; - final RxString rxPass = pw.trim().obs; + final localPasswordSet = + (await bind.mainGetCommon(key: "local-permanent-password-set")) == "true"; + final permanentPasswordSet = + (await bind.mainGetCommon(key: "permanent-password-set")) == "true"; + final presetPassword = permanentPasswordSet && !localPasswordSet; + var canSubmit = false; + final RxString rxPass = "".obs; final rules = [ DigitValidationRule(), UppercaseValidationRule(), @@ -922,9 +927,21 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { MinCharactersValidationRule(8), ]; final maxLength = bind.mainMaxEncryptLen(); + final statusTip = localPasswordSet + ? translate('password-hidden-tip') + : (presetPassword ? translate('preset-password-in-use-tip') : ''); + final showStatusTipOnMobile = + statusTip.isNotEmpty && !isDesktop && !isWebDesktop; gFFI.dialogManager.show((setState, close, context) { - submit() { + updateCanSubmit() { + canSubmit = p0.text.trim().isNotEmpty || p1.text.trim().isNotEmpty; + } + + submit() async { + if (!canSubmit) { + return; + } setState(() { errMsg0 = ""; errMsg1 = ""; @@ -947,7 +964,13 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { }); return; } - bind.mainSetPermanentPassword(password: pass); + final ok = await bind.mainSetPermanentPasswordWithResult(password: pass); + if (!ok) { + setState(() { + errMsg0 = '${translate('Prompt')}: ${translate("Failed")}'; + }); + return; + } if (pass.isNotEmpty) { notEmptyCallback?.call(); } @@ -955,14 +978,20 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { } return CustomAlertDialog( - title: Text(translate("Set Password")), + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.key, color: MyTheme.accent), + Text(translate("Set Password")).paddingOnly(left: 10), + ], + ), content: ConstrainedBox( constraints: const BoxConstraints(minWidth: 500), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const SizedBox( - height: 8.0, + SizedBox( + height: showStatusTipOnMobile ? 0.0 : 6.0, ), Row( children: [ @@ -978,6 +1007,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { rxPass.value = value.trim(); setState(() { errMsg0 = ''; + updateCanSubmit(); }); }, maxLength: maxLength, @@ -989,9 +1019,9 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { children: [ Expanded(child: PasswordStrengthIndicator(password: rxPass)), ], - ).marginSymmetric(vertical: 8), - const SizedBox( - height: 8.0, + ).marginOnly(top: 2, bottom: showStatusTipOnMobile ? 2 : 8), + SizedBox( + height: showStatusTipOnMobile ? 0.0 : 8.0, ), Row( children: [ @@ -1005,6 +1035,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { onChanged: (value) { setState(() { errMsg1 = ''; + updateCanSubmit(); }); }, maxLength: maxLength, @@ -1012,11 +1043,23 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { ), ], ), - const SizedBox( - height: 8.0, + if (statusTip.isNotEmpty) + Row( + children: [ + Icon(Icons.info, color: Colors.amber, size: 18) + .marginOnly(right: 6), + Expanded( + child: Text( + statusTip, + style: const TextStyle(fontSize: 13, height: 1.1), + )) + ], + ).marginOnly(top: 6, bottom: 2), + SizedBox( + height: showStatusTipOnMobile ? 0.0 : 8.0, ), Obx(() => Wrap( - runSpacing: 8, + runSpacing: showStatusTipOnMobile ? 2.0 : 8.0, spacing: 4, children: rules.map((e) { var checked = e.validate(rxPass.value.trim()); @@ -1036,11 +1079,67 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { ], ), ), - actions: [ - dialogButton("Cancel", onPressed: close, isOutline: true), - dialogButton("OK", onPressed: submit), - ], - onSubmit: submit, + actions: (() { + final cancelButton = dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: close, + isOutline: true, + ); + final removeButton = dialogButton( + "Remove", + icon: Icon(Icons.delete_outline_rounded), + onPressed: () async { + setState(() { + errMsg0 = ""; + errMsg1 = ""; + }); + final ok = + await bind.mainSetPermanentPasswordWithResult(password: ""); + if (!ok) { + setState(() { + errMsg0 = '${translate('Prompt')}: ${translate("Failed")}'; + }); + return; + } + close(); + }, + buttonStyle: ButtonStyle( + backgroundColor: MaterialStatePropertyAll(Colors.red)), + ); + final okButton = dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: canSubmit ? submit : null, + ); + if (!isDesktop && !isWebDesktop && localPasswordSet) { + return [ + Align( + alignment: Alignment.centerRight, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerRight, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + cancelButton, + const SizedBox(width: 4), + removeButton, + const SizedBox(width: 4), + okButton, + ], + ), + ), + ), + ]; + } + return [ + cancelButton, + if (localPasswordSet) removeButton, + okButton, + ]; + })(), + onSubmit: canSubmit ? submit : null, onCancel: close, ); }); diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 029629b24..d118b6793 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1109,8 +1109,9 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { if (value == passwordValues[passwordKeys .indexOf(kUsePermanentPassword)] && - (await bind.mainGetPermanentPassword()) - .isEmpty) { + (await bind.mainGetCommon( + key: "permanent-password-set")) != + "true") { if (isChangePermanentPasswordDisabled()) { await callback(); return; diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index 57856a4d7..2c8b0f2d6 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -150,7 +150,8 @@ class _DropDownAction extends StatelessWidget { } if (value == kUsePermanentPassword && - (await bind.mainGetPermanentPassword()).isEmpty) { + (await bind.mainGetCommon(key: "permanent-password-set")) != + "true") { if (isChangePermanentPasswordDisabled()) { callback(); return; diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index f6900e5dd..8b645bb88 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -12,100 +12,6 @@ void _showSuccess() { showToast(translate("Successful")); } -void _showError() { - showToast(translate("Error")); -} - -void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async { - final pw = await bind.mainGetPermanentPassword(); - final p0 = TextEditingController(text: pw); - final p1 = TextEditingController(text: pw); - var validateLength = false; - var validateSame = false; - dialogManager.show((setState, close, context) { - submit() async { - close(); - dialogManager.showLoading(translate("Waiting")); - if (await gFFI.serverModel.setPermanentPassword(p0.text)) { - dialogManager.dismissAll(); - _showSuccess(); - } else { - dialogManager.dismissAll(); - _showError(); - } - } - - return CustomAlertDialog( - title: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.password_rounded, color: MyTheme.accent), - Text(translate('Set your own password')).paddingOnly(left: 10), - ], - ), - content: Form( - autovalidateMode: AutovalidateMode.onUserInteraction, - child: Column(mainAxisSize: MainAxisSize.min, children: [ - TextFormField( - autofocus: true, - obscureText: true, - keyboardType: TextInputType.visiblePassword, - decoration: InputDecoration( - labelText: translate('Password'), - ), - controller: p0, - validator: (v) { - if (v == null) return null; - final val = v.trim().length > 5; - if (validateLength != val) { - // use delay to make setState success - Future.delayed(Duration(microseconds: 1), - () => setState(() => validateLength = val)); - } - return val - ? null - : translate('Too short, at least 6 characters.'); - }, - ).workaroundFreezeLinuxMint(), - TextFormField( - obscureText: true, - keyboardType: TextInputType.visiblePassword, - decoration: InputDecoration( - labelText: translate('Confirmation'), - ), - controller: p1, - validator: (v) { - if (v == null) return null; - final val = p0.text == v; - if (validateSame != val) { - Future.delayed(Duration(microseconds: 1), - () => setState(() => validateSame = val)); - } - return val - ? null - : translate('The confirmation is not identical.'); - }, - ).workaroundFreezeLinuxMint(), - ])), - onCancel: close, - onSubmit: (validateLength && validateSame) ? submit : null, - actions: [ - dialogButton( - 'Cancel', - icon: Icon(Icons.close_rounded), - onPressed: close, - isOutline: true, - ), - dialogButton( - 'OK', - icon: Icon(Icons.done_rounded), - onPressed: (validateLength && validateSame) ? submit : null, - ), - ], - ); - }); -} - void setTemporaryPasswordLengthDialog( OverlayDialogManager dialogManager) async { List lengths = ['6', '8', '10']; diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 5892ed0fe..78e334d4f 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -471,17 +471,6 @@ class ServerModel with ChangeNotifier { WakelockManager.disable(_wakelockKey); } - Future setPermanentPassword(String newPW) async { - await bind.mainSetPermanentPassword(password: newPW); - await Future.delayed(Duration(milliseconds: 500)); - final pw = await bind.mainGetPermanentPassword(); - if (newPW == pw) { - return true; - } else { - return false; - } - } - fetchID() async { final id = await bind.mainGetMyId(); if (id != _serverId.id) { diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 66191d004..1cfce661b 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -1159,10 +1159,6 @@ class RustdeskImpl { return Future.value(''); } - Future mainGetPermanentPassword({dynamic hint}) { - return Future.value(''); - } - Future mainGetFingerprint({dynamic hint}) { return Future.value(''); } @@ -1346,9 +1342,9 @@ class RustdeskImpl { throw UnimplementedError("mainUpdateTemporaryPassword"); } - Future mainSetPermanentPassword( + Future mainSetPermanentPasswordWithResult( {required String password, dynamic hint}) { - throw UnimplementedError("mainSetPermanentPassword"); + throw UnimplementedError("mainSetPermanentPasswordWithResult"); } Future mainCheckSuperUserPermission({dynamic hint}) { diff --git a/libs/hbb_common b/libs/hbb_common index 6fb03d076..f08ce5d6d 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 6fb03d076eae81e244db72e87474eee149a0fb85 +Subproject commit f08ce5d6d07cd200713418ce2932769d14ff21d2 diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 092e6d295..e29133687 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1693,8 +1693,8 @@ pub fn main_get_temporary_password() -> String { ui_interface::temporary_password() } -pub fn main_get_permanent_password() -> String { - ui_interface::permanent_password() +pub fn main_set_permanent_password_with_result(password: String) -> bool { + ui_interface::set_permanent_password_with_result(password) } pub fn main_get_fingerprint() -> String { @@ -2072,10 +2072,6 @@ pub fn main_update_temporary_password() { update_temporary_password(); } -pub fn main_set_permanent_password(password: String) { - set_permanent_password(password); -} - pub fn main_check_super_user_permission() -> bool { check_super_user_permission() } @@ -2423,16 +2419,23 @@ pub fn is_disable_installation() -> SyncReturn { } pub fn is_preset_password() -> bool { - config::HARD_SETTINGS + let hard = config::HARD_SETTINGS .read() .unwrap() .get("password") - .map_or(false, |p| { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - return p == &crate::ipc::get_permanent_password(); - #[cfg(any(target_os = "android", target_os = "ios"))] - return p == &config::Config::get_permanent_password(); - }) + .cloned() + .unwrap_or_default(); + if hard.is_empty() { + return false; + } + + // On desktop, service owns the authoritative config; query it via IPC and return only a boolean. + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return crate::ipc::is_permanent_password_preset(); + + // On mobile, we have no service IPC; verify against local storage. + #[cfg(any(target_os = "android", target_os = "ios"))] + return config::Config::matches_permanent_password_plain(&hard); } // Don't call this function for desktop version. @@ -2768,6 +2771,10 @@ pub fn main_get_common(key: String) -> String { return crate::platform::linux::has_gnome_shortcuts_inhibitor_permission().to_string(); #[cfg(not(target_os = "linux"))] return false.to_string(); + } else if key == "permanent-password-set" { + return ui_interface::is_permanent_password_set().to_string(); + } else if key == "local-permanent-password-set" { + return ui_interface::is_local_permanent_password_set().to_string(); } else { if key.starts_with("download-data-") { let id = key.replace("download-data-", ""); diff --git a/src/ipc.rs b/src/ipc.rs index 891ec81dd..099c24d34 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -632,8 +632,29 @@ async fn handle(data: Data, stream: &mut Connection) { value = Some(Config::get_id()); } else if name == "temporary-password" { value = Some(password::temporary_password()); - } else if name == "permanent-password" { - value = Some(Config::get_permanent_password()); + } else if name == "permanent-password-storage-and-salt" { + let (storage, salt) = Config::get_local_permanent_password_storage_and_salt(); + value = Some(storage + "\n" + &salt); + } else if name == "permanent-password-set" { + value = Some(if Config::has_permanent_password() { + "Y".to_owned() + } else { + "N".to_owned() + }); + } else if name == "permanent-password-is-preset" { + let hard = config::HARD_SETTINGS + .read() + .unwrap() + .get("password") + .cloned() + .unwrap_or_default(); + let is_preset = + !hard.is_empty() && Config::matches_permanent_password_plain(&hard); + value = Some(if is_preset { + "Y".to_owned() + } else { + "N".to_owned() + }); } else if name == "salt" { value = Some(Config::get_salt()); } else if name == "rendezvous_server" { @@ -669,13 +690,24 @@ async fn handle(data: Data, stream: &mut Connection) { allow_err!(stream.send(&Data::Config((name, value))).await); } Some(value) => { + let mut updated = true; if name == "id" { Config::set_key_confirmed(false); Config::set_id(&value); } else if name == "temporary-password" { password::update_temporary_password(); } else if name == "permanent-password" { - Config::set_permanent_password(&value); + if Config::is_disable_change_permanent_password() { + log::warn!("Changing permanent password is disabled"); + updated = false; + } else { + Config::set_permanent_password(&value); + } + // Explicitly ACK/NACK permanent-password writes. This allows UIs/FFI to + // distinguish "accepted by daemon" vs "IPC send succeeded" without + // reading back any secret. + let ack = if updated { "Y" } else { "N" }.to_owned(); + allow_err!(stream.send(&Data::Config((name.clone(), Some(ack)))).await); } else if name == "salt" { Config::set_salt(&value); } else if name == "voice-call-input" { @@ -685,7 +717,9 @@ async fn handle(data: Data, stream: &mut Connection) { } else { return; } - log::info!("{} updated", name); + if updated { + log::info!("{} updated", name); + } } }, Data::Options(value) => match value { @@ -1143,13 +1177,57 @@ pub fn update_temporary_password() -> ResultType<()> { set_config("temporary-password", "".to_owned()) } -pub fn get_permanent_password() -> String { - if let Ok(Some(v)) = get_config("permanent-password") { - Config::set_permanent_password(&v); - v - } else { - Config::get_permanent_password() +fn apply_permanent_password_storage_and_salt_payload(payload: Option<&str>) -> ResultType<()> { + let Some(payload) = payload else { + return Ok(()); + }; + let Some((storage, salt)) = payload.split_once('\n') else { + bail!("Invalid permanent-password-storage-and-salt payload"); + }; + + if storage.is_empty() { + Config::set_permanent_password_storage_for_sync("", "")?; + return Ok(()); } + + Config::set_permanent_password_storage_for_sync(storage, salt)?; + Ok(()) +} + +pub fn sync_permanent_password_storage_from_daemon() -> ResultType<()> { + let v = get_config("permanent-password-storage-and-salt")?; + apply_permanent_password_storage_and_salt_payload(v.as_deref()) +} + +async fn sync_permanent_password_storage_from_daemon_async() -> ResultType<()> { + let ms_timeout = 1_000; + let v = get_config_async("permanent-password-storage-and-salt", ms_timeout).await?; + apply_permanent_password_storage_and_salt_payload(v.as_deref()) +} + +pub fn is_permanent_password_set() -> bool { + match get_config("permanent-password-set") { + Ok(Some(v)) => { + let v = v.trim(); + return v == "Y"; + } + Ok(None) => { + // No response/value (timeout). + } + Err(_) => { + // Connection error. + } + } + log::warn!("Failed to query permanent password state from daemon"); + false +} + +pub fn is_permanent_password_preset() -> bool { + if let Ok(Some(v)) = get_config("permanent-password-is-preset") { + let v = v.trim(); + return v == "Y"; + } + false } pub fn get_fingerprint() -> String { @@ -1159,8 +1237,41 @@ pub fn get_fingerprint() -> String { } pub fn set_permanent_password(v: String) -> ResultType<()> { - Config::set_permanent_password(&v); - set_config("permanent-password", v) + if Config::is_disable_change_permanent_password() { + bail!("Changing permanent password is disabled"); + } + if set_permanent_password_with_ack(v)? { + Ok(()) + } else { + bail!("Changing permanent password was rejected by daemon"); + } +} + +#[tokio::main(flavor = "current_thread")] +pub async fn set_permanent_password_with_ack(v: String) -> ResultType { + set_permanent_password_with_ack_async(v).await +} + +async fn set_permanent_password_with_ack_async(v: String) -> ResultType { + // The daemon ACK/NACK is expected quickly since it applies the config in-process. + let ms_timeout = 1_000; + let mut c = connect(ms_timeout, "").await?; + c.send_config("permanent-password", v).await?; + if let Some(Data::Config((name2, Some(v)))) = c.next_timeout(ms_timeout).await? { + if name2 == "permanent-password" { + let v = v.trim(); + let ok = v == "Y"; + if ok { + // Ensure the hashed permanent password storage is written to the user config file. + // This sync must not affect the daemon ACK outcome. + if let Err(err) = sync_permanent_password_storage_from_daemon_async().await { + log::warn!("Failed to sync permanent password storage from daemon: {err}"); + } + } + return Ok(ok); + } + } + Ok(false) } #[cfg(feature = "flutter")] diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 8af320864..8204da6fd 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "متابعة مع {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 6735d3eff..6c6a13315 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Працягнуць з {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index e87322b8b..218070291 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Продължи с {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index fd78c3ae6..2f1cc8734 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Continua amb {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index b4026bdf9..75d16ff92 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "传入会话期间保持屏幕常亮"), ("Continue with {}", "使用 {} 登录"), ("Display Name", "显示名称"), + ("password-hidden-tip", "永久密码已设置(已隐藏)"), + ("preset-password-in-use-tip", "当前使用预设密码"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 952a55b6c..7b3dc7908 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Pokračovat s {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index d309fff3f..06ad254c7 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Fortsæt med {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index ff4139559..7eca199cb 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Bildschirm während eingehender Sitzungen aktiv halten"), ("Continue with {}", "Fortfahren mit {}"), ("Display Name", "Anzeigename"), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index ab5c6dfa7..38e11bfce 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Διατήρηση ενεργής οθόνης κατά τη διάρκεια των εισερχόμενων συνεδριών"), ("Continue with {}", "Συνέχεια με {}"), ("Display Name", "Εμφανιζόμενο όνομα"), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index d8190bde0..73974a2e5 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -272,5 +272,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("rel-mouse-permission-lost-tip", "Keyboard permission was revoked. Relative Mouse Mode has been disabled."), ("keep-awake-during-outgoing-sessions-label", "Keep screen awake during outgoing sessions"), ("keep-awake-during-incoming-sessions-label", "Keep screen awake during incoming sessions"), + ("password-hidden-tip", "Permanent password is set (hidden)."), + ("preset-password-in-use-tip", "Preset password is currently in use."), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index be7d8a751..921f79612 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", ""), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 524c9a98e..0f49079a2 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Continuar con {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index 3a90a1bd7..d65cd31c5 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Jätka koos {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index 04bed674a..f12ecf371 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "{} honekin jarraitu"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 6de3960a9..5f6d5f005 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "ادامه با {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fi.rs b/src/lang/fi.rs index 3dc01b4d5..43c033a11 100644 --- a/src/lang/fi.rs +++ b/src/lang/fi.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Jatka käyttäen {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 56b19a33d..0dda7817f 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Maintenir l’écran allumé lors des sessions entrantes"), ("Continue with {}", "Continuer avec {}"), ("Display Name", "Nom d’affichage"), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ge.rs b/src/lang/ge.rs index 8afb46704..dc78bc0d9 100644 --- a/src/lang/ge.rs +++ b/src/lang/ge.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "{}-ით გაგრძელება"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 1e2d84b71..741805e25 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "המשך עם {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 8ae5d2d96..2d596bacc 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Nastavi sa {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 5486d16b4..e69514e45 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Képernyő aktív állapotban tartása a bejövő munkamenetek során"), ("Continue with {}", "Folytatás ezzel: {}"), ("Display Name", "Kijelző név"), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index a19d9ad85..356a9ee2d 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Lanjutkan dengan {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index bc2b98eb6..a577971a9 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Mantieni lo schermo attivo durante le sessioni in ingresso"), ("Continue with {}", "Continua con {}"), ("Display Name", "Visualizza nome"), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index c933c8018..805898ef9 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "{} で続行"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 15cfd10ef..51a18ceb7 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "수신 세션 중 화면 켜짐 유지"), ("Continue with {}", "{}(으)로 계속"), ("Display Name", "표시 이름"), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index e3d31cde6..e943ff4cd 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", ""), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 28451dc6f..a4f39f1e4 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Tęsti su {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 9c03bf8fc..838984207 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Turpināt ar {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index daf1c90d2..d9cf6ad38 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Fortsett med {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index e999e99ff..77da4f79e 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Houd het scherm open tijdens de inkomende sessies."), ("Continue with {}", "Ga verder met {}"), ("Display Name", "Naam Weergeven"), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 96cf22d22..51611c9b3 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Utrzymuj urządzenie w stanie aktywnym podczas sesji przychodzących"), ("Continue with {}", "Kontynuuj z {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 8637ce63c..0cdcf93b4 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", ""), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index b642cd75a..f9bae32b1 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Manter tela ativa durante sessões de entrada"), ("Continue with {}", "Continuar com {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 69f72f316..0a5ab0299 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Continuă cu {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index c28baf600..5712c1fcd 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Не отключать экран во время входящих сеансов"), ("Continue with {}", "Продолжить с {}"), ("Display Name", "Отображаемое имя"), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index c67d88f99..f2c4fbfa2 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Sighi cun {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 9132485ac..d0e99b2a4 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Pokračovať s {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 6dfb5d572..aef6b7c66 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Nadaljuj z {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 0cfdb03be..5f9d5505b 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Vazhdo me {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 743aacc2c..19ae6896f 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Nastavi sa {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 52a451474..7ad257fcb 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Fortsätt med {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs index e4be24259..2cee45268 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "{} உடன் தொடர்"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index b70bb616b..ff755768c 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", ""), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index f4bf65798..2d3eb1d34 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "ทำต่อด้วย {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 3f7c21c2b..d69995b5f 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Gelen oturumlar süresince ekranı açık tutun"), ("Continue with {}", "{} ile devam et"), ("Display Name", "Görünen Ad"), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 1172fe2cc..4089257cc 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "在連入工作階段期間保持螢幕喚醒"), ("Continue with {}", "使用 {} 登入"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 146b89569..2594b7cc3 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Продовжити з {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vi.rs b/src/lang/vi.rs index 6ba287912..6939b2ea1 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -741,5 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", ""), ("Continue with {}", "Tiếp tục với {}"), ("Display Name", ""), + ("password-hidden-tip", ""), + ("preset-password-in-use-tip", ""), ].iter().cloned().collect(); } diff --git a/src/server/connection.rs b/src/server/connection.rs index 1ffb1a25e..afa40a25b 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -27,6 +27,7 @@ use hbb_common::platform::linux::run_cmds; #[cfg(target_os = "android")] use hbb_common::protobuf::EnumOrUnknown; use hbb_common::{ + config::decode_permanent_password_h1_from_storage, config::{self, keys, Config, TrustedDevice}, fs::{self, can_enable_overwrite_detection, JobType}, futures::{SinkExt, StreamExt}, @@ -77,6 +78,18 @@ lazy_static::lazy_static! { static ref WAKELOCK_KEEP_AWAKE_OPTION: Arc::>> = Default::default(); } +fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { + if a.len() != b.len() { + return false; + } + // Avoid data-dependent early exits. + let mut x: u8 = 0; + for i in 0..a.len() { + x |= a[i] ^ b[i]; + } + x == 0 +} + #[cfg(any(target_os = "windows", target_os = "linux"))] lazy_static::lazy_static! { static ref WALLPAPER_REMOVER: Arc>> = Default::default(); @@ -1969,23 +1982,53 @@ impl Connection { self.tx_input.send(MessageInput::Key((msg, press))).ok(); } - fn validate_one_password(&self, password: String) -> bool { - if password.len() == 0 { + fn verify_h1(&self, h1: &[u8]) -> bool { + let mut hasher2 = Sha256::new(); + hasher2.update(h1); + hasher2.update(self.hash.challenge.as_bytes()); + // A normal `==` on slices may short-circuit on the first mismatch, which can leak how many leading + // bytes matched via timing. In typical remote scenarios this is difficult to exploit due to network + // jitter, changing challenges, and login attempt throttling, but a constant-time comparison here is + // low-cost defensive programming. + constant_time_eq(&hasher2.finalize()[..], &self.lr.password[..]) + } + + #[inline] + fn validate_one_password(&self, password: &str) -> bool { + self.validate_password_plain(password) + } + + fn validate_password_plain(&self, password: &str) -> bool { + if password.is_empty() { return false; } + let mut hasher = Sha256::new(); - hasher.update(password); - hasher.update(&self.hash.salt); - let mut hasher2 = Sha256::new(); - hasher2.update(&hasher.finalize()[..]); - hasher2.update(&self.hash.challenge); - hasher2.finalize()[..] == self.lr.password[..] + hasher.update(password.as_bytes()); + hasher.update(self.hash.salt.as_bytes()); + let h1_plain = hasher.finalize(); + self.verify_h1(&h1_plain[..]) + } + + fn validate_password_storage(&self, storage: &str) -> bool { + if storage.is_empty() { + return false; + } + + // Use strict decode success to detect hashed storage. + // If decode fails, treat as legacy plaintext storage for compatibility. + if let Some(h1) = decode_permanent_password_h1_from_storage(storage) { + return self.verify_h1(&h1[..]); + } + + // Legacy plaintext storage path. + self.validate_password_plain(storage) } fn validate_password(&mut self) -> bool { if password::temporary_enabled() { let password = password::temporary_password(); - if self.validate_one_password(password.clone()) { + if self.validate_one_password(&password) { raii::AuthedConnID::update_or_insert_session( self.session_key(), Some(password), @@ -1995,8 +2038,24 @@ impl Connection { } } if password::permanent_enabled() { - if self.validate_one_password(Config::get_permanent_password()) { - return true; + // Since hashed storage uses a prefix-based encoding, a hard plaintext that + // happens to look like hashed storage could be mis-detected. Validate local storage + // and hard/preset plaintext via separate paths to avoid that ambiguity. + let (local_storage, _) = Config::get_local_permanent_password_storage_and_salt(); + if !local_storage.is_empty() { + if self.validate_password_storage(&local_storage) { + return true; + } + } else { + let hard = config::HARD_SETTINGS + .read() + .unwrap() + .get("password") + .cloned() + .unwrap_or_default(); + if !hard.is_empty() && self.validate_password_plain(&hard) { + return true; + } } } false @@ -2016,7 +2075,7 @@ impl Connection { if let Some(session) = session { if !self.lr.password.is_empty() && (tfa && session.tfa - || !tfa && self.validate_one_password(session.random_password.clone())) + || !tfa && self.validate_password_plain(&session.random_password)) { log::info!("is recent session"); return true; diff --git a/src/ui.rs b/src/ui.rs index fc59cffd2..154319ce4 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -212,12 +212,16 @@ impl UI { update_temporary_password() } - fn permanent_password(&self) -> String { - permanent_password() + fn set_permanent_password(&self, password: String) { + let _ = set_permanent_password_with_result(password); } - fn set_permanent_password(&self, password: String) { - set_permanent_password(password); + fn is_local_permanent_password_set(&self) -> bool { + is_local_permanent_password_set() + } + + fn is_permanent_password_set(&self) -> bool { + is_permanent_password_set() } fn get_remote_id(&mut self) -> String { @@ -726,8 +730,9 @@ impl sciter::EventHandler for UI { fn get_id(); fn temporary_password(); fn update_temporary_password(); - fn permanent_password(); fn set_permanent_password(String); + fn is_local_permanent_password_set(); + fn is_permanent_password_set(); fn get_remote_id(); fn set_remote_id(String); fn closing(i32, i32, i32, i32); diff --git a/src/ui/common.css b/src/ui/common.css index 3307e0965..16dd6ca9f 100644 --- a/src/ui/common.css +++ b/src/ui/common.css @@ -72,6 +72,11 @@ button.button:hover, button.outline:hover { border-color: color(hover-border); } +button:disabled, +button:disabled:hover { + opacity: 0.3; +} + button.link { background: none !important; border: none; @@ -484,4 +489,4 @@ div.user-session select { background: color(bg); color: color(text); padding-left: 0.5em; -} \ No newline at end of file +} diff --git a/src/ui/index.tis b/src/ui/index.tis index acec6a2b5..be826529d 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -1072,6 +1072,7 @@ class PasswordArea: Reactor.Component { var method = handler.get_option('verification-method'); var approve_mode= handler.get_option('approve-mode'); var show_password = approve_mode != 'click'; + var has_local_password = handler.is_local_permanent_password_set(); return
  • {svg_checkmark}{translate('Accept sessions via password')}
  • {svg_checkmark}{translate('Accept sessions via click')}
  • @@ -1082,6 +1083,7 @@ class PasswordArea: Reactor.Component { { !show_password ? '' :
  • {svg_checkmark}{translate('Use both passwords')}
  • } { !show_password ? '' :
    } { !show_password || disable_change_permanent_password ? '' :
  • {translate('Set permanent password')}
  • } + { !show_password || disable_change_permanent_password ? '' :
  • {translate('Clear permanent password')}
  • } { !show_password ? '' : }
  • {svg_checkmark}{translate('enable-2fa-title')}
  • @@ -1114,6 +1116,10 @@ class PasswordArea: Reactor.Component { el.state.disabled = true; } } + if (el.id == "clear-password") { + var has_local_password = handler.is_local_permanent_password_set(); + el.state.disabled = !has_local_password; + } if (el.id == "tfa") el.attributes.toggleClass("selected", has_valid_2fa); } @@ -1129,16 +1135,28 @@ class PasswordArea: Reactor.Component { event click $(li#set-password) { var me = this; - var password = handler.permanent_password(); - var value_field = password.length == 0 ? "" : "value=" + password; + var has_local_password = handler.is_local_permanent_password_set(); + var permanent_password_set = handler.is_permanent_password_set(); + var password_hidden_tip = translate('password-hidden-tip'); + var preset_password_tip = translate('preset-password-in-use-tip'); + var password_tip = ""; + if (has_local_password) { + password_tip = "
    [!] " + password_hidden_tip + "
    "; + } else if (permanent_password_set) { + password_tip = "
    [!] " + preset_password_tip + "
    "; + } msgbox("custom-password", translate("Set Password"), "
    \ -
    " + translate('Password') + ":
    \ -
    " + translate('Confirmation') + ":
    \ +
    " + translate('Password') + ":
    \ +
    " + translate('Confirmation') + ":
    \ + " + password_tip + " \
    \ ", "", function(res=null) { if (!res) return; var p0 = (res.password || "").trim(); var p1 = (res.confirmation || "").trim(); + if (p0.length == 0 && p1.length == 0) { + return " "; + } if (p0.length < 6 && p0.length != 0) { return translate("Too short, at least 6 characters."); } @@ -1148,6 +1166,15 @@ class PasswordArea: Reactor.Component { handler.set_permanent_password(p0); me.update(); }, msgbox_default_height, get_msgbox_width()); + self.timer(30ms, function() { + updateSetPasswordSubmitState(); + }); + } + + event click $(li#clear-password) { + if (this.$(li#clear-password).state.disabled) return; + handler.set_permanent_password(""); + this.update(); } event click $(menu#edit-password-context>li) (_, me) { @@ -1227,6 +1254,18 @@ function updatePasswordArea() { } if (!outgoing_only) updatePasswordArea(); +function updateSetPasswordSubmitState() { + var dialog = $(#msgbox); + if (!dialog) return; + var password = dialog.$(input[name='password']); + var confirmation = dialog.$(input[name='confirmation']); + var submit = dialog.$(button#submit); + if (!password || !confirmation || !submit) return; + var can_submit = (password.value || "").trim().length > 0 || + (confirmation.value || "").trim().length > 0; + submit.state.disabled = !can_submit; +} + class ID: Reactor.Component { function render() { return
    ); event click $(#powered-by) { diff --git a/src/ui/msgbox.tis b/src/ui/msgbox.tis index 542691f5f..6e6b6a62f 100644 --- a/src/ui/msgbox.tis +++ b/src/ui/msgbox.tis @@ -193,8 +193,10 @@ class MsgboxComponent: Reactor.Component { } function submit() { - if (this.$(button#submit)) { - this.$(button#submit).sendEvent("click"); + var submit_btn = this.$(button#submit); + if (submit_btn) { + if (submit_btn.state.disabled) return; + submit_btn.sendEvent("click"); } } diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 49098f2db..1645b242d 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -609,19 +609,57 @@ pub fn update_temporary_password() { } #[inline] -pub fn permanent_password() -> String { +pub fn is_permanent_password_set() -> bool { #[cfg(any(target_os = "android", target_os = "ios"))] - return Config::get_permanent_password(); + return Config::has_permanent_password(); #[cfg(not(any(target_os = "android", target_os = "ios")))] - return ipc::get_permanent_password(); + { + let daemon_is_set = ipc::is_permanent_password_set(); + // `daemon_is_set` is authoritative for the return value. Local storage is only used to + // decide whether we should attempt a sync to clear stale user-side state. + let local_storage_is_empty = if daemon_is_set { + true + } else { + let (storage, _) = Config::get_local_permanent_password_storage_and_salt(); + storage.is_empty() + }; + if daemon_is_set || !local_storage_is_empty { + allow_err!(ipc::sync_permanent_password_storage_from_daemon()); + } + daemon_is_set + } } #[inline] -pub fn set_permanent_password(password: String) { +pub fn is_local_permanent_password_set() -> bool { #[cfg(any(target_os = "android", target_os = "ios"))] - Config::set_permanent_password(&password); + return Config::has_local_permanent_password(); #[cfg(not(any(target_os = "android", target_os = "ios")))] - allow_err!(ipc::set_permanent_password(password)); + { + allow_err!(ipc::sync_permanent_password_storage_from_daemon()); + Config::has_local_permanent_password() + } +} + +pub fn set_permanent_password_with_result(password: String) -> bool { + if config::Config::is_disable_change_permanent_password() { + return false; + } + #[cfg(any(target_os = "android", target_os = "ios"))] + { + config::Config::set_permanent_password(&password); + return true; + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + match crate::ipc::set_permanent_password_with_ack(password) { + Ok(ok) => ok, + Err(err) => { + log::warn!("Failed to set permanent password via IPC: {err}"); + false + } + } + } } #[inline] From f02cd9c0f6a9fdf359ea7a28919b8fb93c86ed21 Mon Sep 17 00:00:00 2001 From: 21pages Date: Fri, 27 Mar 2026 13:22:16 +0800 Subject: [PATCH 191/277] Fix Windows session-based logon and lock-screen detection (#14620) * Fix Windows session-based logon and lock-screen detection - scope LogonUI and locked-state checks to the current Windows session - allow permanent password fallback for logon and lock-screen access Signed-off-by: 21pages * Log permanent-password fallback on logon screen Signed-off-by: 21pages --------- Signed-off-by: 21pages --- src/platform/windows.cc | 3 +-- src/platform/windows.rs | 21 +++++++++++---------- src/server/connection.rs | 20 +++++++++++++++----- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/platform/windows.cc b/src/platform/windows.cc index 74c20c80d..9027d9d89 100644 --- a/src/platform/windows.cc +++ b/src/platform/windows.cc @@ -580,9 +580,8 @@ extern "C" return rdp_or_console; } - BOOL is_session_locked(BOOL include_rdp) + BOOL is_session_locked(DWORD session_id) { - DWORD session_id = get_current_session(include_rdp); if (session_id == 0xFFFFFFFF) { return FALSE; } diff --git a/src/platform/windows.rs b/src/platform/windows.rs index b579891f1..7e4e390aa 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -523,7 +523,7 @@ const SERVICE_TYPE: ServiceType = ServiceType::OWN_PROCESS; extern "C" { fn get_current_session(rdp: BOOL) -> DWORD; - fn is_session_locked(include_rdp: BOOL) -> BOOL; + fn is_session_locked(session_id: DWORD) -> BOOL; fn LaunchProcessWin( cmd: *const u16, session_id: DWORD, @@ -1149,20 +1149,21 @@ pub fn is_prelogin() -> bool { } pub fn is_locked() -> bool { - unsafe { is_session_locked(share_rdp()) == TRUE } + let Some(session_id) = get_current_process_session_id() else { + return false; + }; + unsafe { is_session_locked(session_id) == TRUE } } -// `is_logon_ui()` is regardless of multiple sessions now. -// It only check if "LogonUI.exe" exists. -// -// If there're mulitple sessions (logged in users), -// some are in the login screen, while the others are not. -// Then this function may not work fine if the session we want to handle(connect) is not in the login screen. -// But it's a rare case and cannot be simply handled, so it will not be dealt with for the time being. #[inline] pub fn is_logon_ui() -> ResultType { + let Some(current_sid) = get_current_process_session_id() else { + return Ok(false); + }; let pids = get_pids("LogonUI.exe")?; - Ok(!pids.is_empty()) + Ok(pids + .into_iter() + .any(|pid| get_session_id_of_process(pid) == Some(current_sid))) } pub fn is_root() -> bool { diff --git a/src/server/connection.rs b/src/server/connection.rs index afa40a25b..0e7f26263 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -2025,7 +2025,7 @@ impl Connection { self.validate_password_plain(storage) } - fn validate_password(&mut self) -> bool { + fn validate_password(&mut self, allow_permanent_password: bool) -> bool { if password::temporary_enabled() { let password = password::temporary_password(); if self.validate_one_password(&password) { @@ -2037,13 +2037,19 @@ impl Connection { return true; } } - if password::permanent_enabled() { + if password::permanent_enabled() || allow_permanent_password { + let print_fallback = || { + if allow_permanent_password && !password::permanent_enabled() { + log::info!("Permanent password accepted via logon-screen fallback"); + } + }; // Since hashed storage uses a prefix-based encoding, a hard plaintext that // happens to look like hashed storage could be mis-detected. Validate local storage // and hard/preset plaintext via separate paths to avoid that ambiguity. let (local_storage, _) = Config::get_local_permanent_password_storage_and_salt(); if !local_storage.is_empty() { if self.validate_password_storage(&local_storage) { + print_fallback(); return true; } } else { @@ -2054,6 +2060,7 @@ impl Connection { .cloned() .unwrap_or_default(); if !hard.is_empty() && self.validate_password_plain(&hard) { + print_fallback(); return true; } } @@ -2349,6 +2356,10 @@ impl Connection { #[cfg(any(target_os = "android", target_os = "ios"))] let is_logon = || crate::platform::is_prelogin(); + let allow_logon_screen_password = + crate::get_builtin_option(keys::OPTION_ALLOW_LOGON_SCREEN_PASSWORD) == "Y" + && is_logon(); + if !hbb_common::is_ip_str(&lr.username) && !hbb_common::is_domain_port_str(&lr.username) && lr.username != Config::get_id() @@ -2357,8 +2368,7 @@ impl Connection { .await; return false; } else if (password::approve_mode() == ApproveMode::Click - && !(crate::get_builtin_option(keys::OPTION_ALLOW_LOGON_SCREEN_PASSWORD) == "Y" - && is_logon())) + && !allow_logon_screen_password) || password::approve_mode() == ApproveMode::Both && !password::has_valid_password() { self.try_start_cm(lr.my_id, lr.my_name, false); @@ -2394,7 +2404,7 @@ impl Connection { if !res { return true; } - if !self.validate_password() { + if !self.validate_password(allow_logon_screen_password) { self.update_failure(failure, false, 0); if err_msg.is_empty() { self.send_login_error(crate::client::LOGIN_MSG_PASSWORD_WRONG) From f557fc94fa3dfc688e568c3ff0034826c7e47e1a Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Sat, 28 Mar 2026 06:02:09 +0100 Subject: [PATCH 192/277] Italian language update (#14626) --- src/lang/it.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index a577971a9..1b6e49691 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -741,7 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Mantieni lo schermo attivo durante le sessioni in ingresso"), ("Continue with {}", "Continua con {}"), ("Display Name", "Visualizza nome"), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), + ("password-hidden-tip", "È impostata una password permanente (nascosta)."), + ("preset-password-in-use-tip", "È attualmente in uso la password preimpostata."), ].iter().cloned().collect(); } From 010a54d1c9f6535b828b2302ba354daa654a8ffe Mon Sep 17 00:00:00 2001 From: bilimiyorum <131397022+bilimiyorum@users.noreply.github.com> Date: Sun, 29 Mar 2026 18:02:53 +0300 Subject: [PATCH 193/277] Update tr.rs (#14628) New string entries --- src/lang/tr.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/tr.rs b/src/lang/tr.rs index d69995b5f..5acb15221 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -741,7 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Gelen oturumlar süresince ekranı açık tutun"), ("Continue with {}", "{} ile devam et"), ("Display Name", "Görünen Ad"), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), + ("password-hidden-tip", "Şifre gizli"), + ("preset-password-in-use-tip", "Önceden ayarlanmış şifre kullanılıyor"), ].iter().cloned().collect(); } From d01ce3173f43aa7771e1d71f5af566783c53a3f8 Mon Sep 17 00:00:00 2001 From: solokot Date: Mon, 30 Mar 2026 17:37:35 +0300 Subject: [PATCH 194/277] Update ru.rs (#14636) --- src/lang/ru.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 5712c1fcd..14bc96390 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -666,7 +666,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Incoming Print Job", "Входящее задание печати"), ("use-the-default-printer-tip", "Использовать принтер по умолчанию"), ("use-the-selected-printer-tip", "Использовать выбранный принтер"), - ("auto-print-tip", "Автоматически выполнять печать на выбранном принтере."), + ("auto-print-tip", "Автоматически выполнять печать на выбранном принтере"), ("print-incoming-job-confirm-tip", "Получено задание на печать с удалённого устройства. Выполнить его локально?"), ("remote-printing-disallowed-tile-tip", "Удалённая печать запрещена"), ("remote-printing-disallowed-text-tip", "Настройки разрешений на управляемой стороне запрещают удалённую печать."), @@ -741,7 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Не отключать экран во время входящих сеансов"), ("Continue with {}", "Продолжить с {}"), ("Display Name", "Отображаемое имя"), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), + ("password-hidden-tip", "Установлен постоянный пароль (скрытый)."), + ("preset-password-in-use-tip", "Установленный пароль сейчас используется."), ].iter().cloned().collect(); } From de194417d4836136125e056f9c0451b13985d77b Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Tue, 31 Mar 2026 15:25:05 +0200 Subject: [PATCH 195/277] Update de.rs (#14640) --- src/lang/de.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 7eca199cb..39e077348 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -741,7 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Bildschirm während eingehender Sitzungen aktiv halten"), ("Continue with {}", "Fortfahren mit {}"), ("Display Name", "Anzeigename"), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), + ("password-hidden-tip", "Ein permanentes Passwort wurde festgelegt (ausgeblendet)."), + ("preset-password-in-use-tip", "Das voreingestellte Passwort wird derzeit verwendet."), ].iter().cloned().collect(); } From d135c58ead9e6a19d086f204462d27ba3fc0a88b Mon Sep 17 00:00:00 2001 From: XLion Date: Tue, 31 Mar 2026 21:26:00 +0800 Subject: [PATCH 196/277] Update tw.rs (#14643) --- src/lang/tw.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 4089257cc..5211cc92b 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -740,8 +740,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", "在連出工作階段期間保持螢幕喚醒"), ("keep-awake-during-incoming-sessions-label", "在連入工作階段期間保持螢幕喚醒"), ("Continue with {}", "使用 {} 登入"), - ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), + ("Display Name", "顯示名稱"), + ("password-hidden-tip", "固定密碼已設定(已隱藏)"), + ("preset-password-in-use-tip", "目前正在使用預設密碼"), ].iter().cloned().collect(); } From 9e4b7fca4dde241c83cfbc3da94f4c8c3c12feb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?VenusGirl=E2=9D=A4?= Date: Tue, 31 Mar 2026 22:34:35 +0900 Subject: [PATCH 197/277] Update Korean (#14644) --- src/lang/ko.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 51a18ceb7..7cc0c9067 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -741,7 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "수신 세션 중 화면 켜짐 유지"), ("Continue with {}", "{}(으)로 계속"), ("Display Name", "표시 이름"), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), + ("password-hidden-tip", "영구 비밀번호가 설정되었습니다 (숨김)."), + ("preset-password-in-use-tip", "현재 사전 설정된 비밀번호가 사용 중입니다."), ].iter().cloned().collect(); } From cca6a5fe12570d7a65f1b8eb7485a1c865dcd859 Mon Sep 17 00:00:00 2001 From: Alex Rijckaert Date: Wed, 1 Apr 2026 12:10:39 +0200 Subject: [PATCH 198/277] Update Dutch translations (#14654) --- src/lang/nl.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 77da4f79e..6d140daad 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -741,7 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Houd het scherm open tijdens de inkomende sessies."), ("Continue with {}", "Ga verder met {}"), ("Display Name", "Naam Weergeven"), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), + ("password-hidden-tip", "Er is een permanent wachtwoord ingesteld (verborgen)."), + ("preset-password-in-use-tip", "Het basis wachtwoord is momenteel in gebruik."), ].iter().cloned().collect(); } From 4e30ee8d1cdf224dc45366523b71002fa3ef0cd2 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Fri, 3 Apr 2026 23:13:05 +0800 Subject: [PATCH 199/277] tcp proxy (#14633) * tcp proxy * fix per review * fix per review * Suppress secure_tcp info logs for TCP proxy requests Signed-off-by: 21pages * copilot review: redact tcp proxy logs, dedupe headers, and avoid body clone Signed-off-by: 21pages * format common.rs Signed-off-by: 21pages * copilot review: test function name Signed-off-by: 21pages * copilot review: format IPv6 tcp proxy log targets correctly Signed-off-by: 21pages * copilot review: normalize HTTP method before direct request dispatch Signed-off-by: 21pages * review: extract fallback helper, fix Content-Type override, add overall timeout - Extract duplicated TCP proxy fallback logic into generic `with_tcp_proxy_fallback` helper used by both `post_request` and `http_request_sync`, eliminating code drift risk - Allow caller-supplied Content-Type to override the default in `parse_simple_header` instead of silently dropping it - Take body by reference in `post_request_http` to avoid eager clone when no fallback is needed - Wrap entire `tcp_proxy_request` flow (connect + handshake + send + receive) in an overall timeout to prevent indefinite stalls Co-Authored-By: Claude Opus 4.6 * review: make is_public case-insensitive and cover mixed-case rustdesk URLs Signed-off-by: 21pages * oidc: route auth requests through shared HTTP/tcp-proxy path while keeping TLS warmup Signed-off-by: 21pages * refactor: replace unused TryFrom with HbbHttpResponse::parse method Remove TryFrom impl that was never called and replace the private parse_hbb_http_response helper in account.rs with a public parse() method on HbbHttpResponse, eliminating code duplication. Signed-off-by: 21pages --------- Signed-off-by: 21pages Co-authored-by: 21pages Co-authored-by: Claude Opus 4.6 --- src/common.rs | 588 +++++++++++++++++++++++++++++++++++---- src/hbbs_http.rs | 10 +- src/hbbs_http/account.rs | 61 ++-- 3 files changed, 571 insertions(+), 88 deletions(-) diff --git a/src/common.rs b/src/common.rs index 3e23770c6..69e3ec304 100644 --- a/src/common.rs +++ b/src/common.rs @@ -39,7 +39,7 @@ use hbb_common::{ use crate::{ hbbs_http::{create_http_client_async, get_url_for_tls}, - ui_interface::{get_option, is_installed, set_option}, + ui_interface::{get_api_server as ui_get_api_server, get_option, is_installed, set_option}, }; #[derive(Debug, Eq, PartialEq)] @@ -1086,6 +1086,7 @@ fn get_api_server_(api: String, custom: String) -> String { #[inline] pub fn is_public(url: &str) -> bool { + let url = url.to_ascii_lowercase(); url.contains("rustdesk.com/") || url.ends_with("rustdesk.com") } @@ -1123,22 +1124,286 @@ pub fn get_audit_server(api: String, custom: String, typ: String) -> String { format!("{}/api/audit/{}", url, typ) } -pub async fn post_request(url: String, body: String, header: &str) -> ResultType { +/// Check if we should use raw TCP proxy for API calls. +/// Returns true if USE_RAW_TCP_FOR_API builtin option is "Y", WebSocket is off, +/// and the target URL belongs to the configured non-public API host. +#[inline] +fn should_use_raw_tcp_for_api(url: &str) -> bool { + get_builtin_option(keys::OPTION_USE_RAW_TCP_FOR_API) == "Y" + && !use_ws() + && is_tcp_proxy_api_target(url) +} + +/// Check if we can attempt raw TCP proxy fallback for this target URL. +#[inline] +fn can_fallback_to_raw_tcp(url: &str) -> bool { + !use_ws() && is_tcp_proxy_api_target(url) +} + +#[inline] +fn should_use_tcp_proxy_for_api_url(url: &str, api_url: &str) -> bool { + if api_url.is_empty() || is_public(api_url) { + return false; + } + + let target_host = url::Url::parse(url) + .ok() + .and_then(|parsed| parsed.host_str().map(|host| host.to_ascii_lowercase())); + let api_host = url::Url::parse(api_url) + .ok() + .and_then(|parsed| parsed.host_str().map(|host| host.to_ascii_lowercase())); + + matches!((target_host, api_host), (Some(target), Some(api)) if target == api) +} + +#[inline] +fn is_tcp_proxy_api_target(url: &str) -> bool { + should_use_tcp_proxy_for_api_url(url, &ui_get_api_server()) +} + +fn tcp_proxy_log_target(url: &str) -> String { + url::Url::parse(url) + .ok() + .map(|parsed| { + let mut redacted = format!("{}://", parsed.scheme()); + let Some(host) = parsed.host() else { + return "".to_owned(); + }; + redacted.push_str(&host.to_string()); + if let Some(port) = parsed.port() { + redacted.push(':'); + redacted.push_str(&port.to_string()); + } + redacted.push_str(parsed.path()); + redacted + }) + .unwrap_or_else(|| "".to_owned()) +} + +#[inline] +fn get_tcp_proxy_addr() -> String { + check_port(Config::get_rendezvous_server(), RENDEZVOUS_PORT) +} + +/// Send an HTTP request via the rendezvous server's TCP proxy using protobuf. +/// Connects with `connect_tcp` + `secure_tcp`, sends `HttpProxyRequest`, +/// receives `HttpProxyResponse`. +/// +/// The entire operation (connect + handshake + send + receive) is wrapped in +/// an overall timeout of `CONNECT_TIMEOUT + READ_TIMEOUT` so that a stall at +/// any stage cannot block the caller indefinitely. +async fn tcp_proxy_request( + method: &str, + url: &str, + body: &[u8], + headers: Vec, +) -> ResultType { + let tcp_addr = get_tcp_proxy_addr(); + if tcp_addr.is_empty() { + bail!("No rendezvous server configured for TCP proxy"); + } + + let parsed = url::Url::parse(url)?; + let path = if let Some(query) = parsed.query() { + format!("{}?{}", parsed.path(), query) + } else { + parsed.path().to_string() + }; + + log::debug!( + "Sending {} {} via TCP proxy to {}", + method, + parsed.path(), + tcp_addr + ); + + let overall_timeout = CONNECT_TIMEOUT + READ_TIMEOUT; + timeout(overall_timeout, async { + let mut conn = socket_client::connect_tcp(&*tcp_addr, CONNECT_TIMEOUT).await?; + let key = crate::get_key(true).await; + secure_tcp_silent(&mut conn, &key).await?; + + let mut req = HttpProxyRequest::new(); + req.method = method.to_uppercase(); + req.path = path; + req.headers = headers.into(); + req.body = Bytes::from(body.to_vec()); + + let mut msg_out = RendezvousMessage::new(); + msg_out.set_http_proxy_request(req); + conn.send(&msg_out).await?; + + match conn.next().await { + Some(Ok(bytes)) => { + let msg_in = RendezvousMessage::parse_from_bytes(&bytes)?; + match msg_in.union { + Some(rendezvous_message::Union::HttpProxyResponse(resp)) => Ok(resp), + _ => bail!("Unexpected response from TCP proxy"), + } + } + Some(Err(e)) => bail!("TCP proxy read error: {}", e), + None => bail!("TCP proxy connection closed without response"), + } + }) + .await? +} + +/// Build HeaderEntry list from "Key: Value" style header string (used by post_request). +/// If the caller supplies a Content-Type header it overrides the default `application/json`. +fn parse_simple_header(header: &str) -> Vec { + let mut entries = Vec::new(); + let mut has_content_type = false; + if !header.is_empty() { + let tmp: Vec<&str> = header.splitn(2, ": ").collect(); + if tmp.len() == 2 { + if tmp[0].eq_ignore_ascii_case("Content-Type") { + has_content_type = true; + } + entries.push(HeaderEntry { + name: tmp[0].into(), + value: tmp[1].into(), + ..Default::default() + }); + } + } + if !has_content_type { + entries.insert( + 0, + HeaderEntry { + name: "Content-Type".into(), + value: "application/json".into(), + ..Default::default() + }, + ); + } + entries +} + +/// POST request via TCP proxy. +async fn post_request_via_tcp_proxy(url: &str, body: &str, header: &str) -> ResultType { + let headers = parse_simple_header(header); + let resp = tcp_proxy_request("POST", url, body.as_bytes(), headers).await?; + if !resp.error.is_empty() { + bail!("TCP proxy error: {}", resp.error); + } + Ok(String::from_utf8_lossy(&resp.body).to_string()) +} + +fn http_proxy_response_to_json(resp: HttpProxyResponse) -> ResultType { + if !resp.error.is_empty() { + bail!("TCP proxy error: {}", resp.error); + } + + let mut response_headers = Map::new(); + for entry in resp.headers.iter() { + response_headers.insert(entry.name.to_lowercase(), json!(entry.value)); + } + + let mut result = Map::new(); + result.insert("status_code".to_string(), json!(resp.status)); + result.insert("headers".to_string(), Value::Object(response_headers)); + result.insert( + "body".to_string(), + json!(String::from_utf8_lossy(&resp.body)), + ); + + serde_json::to_string(&result).map_err(|e| anyhow!("Failed to serialize response: {}", e)) +} + +fn parse_json_header_entries(header: &str) -> ResultType> { + let v: Value = serde_json::from_str(header)?; + if let Value::Object(obj) = v { + Ok(obj + .iter() + .map(|(key, value)| HeaderEntry { + name: key.clone(), + value: value.as_str().unwrap_or_default().into(), + ..Default::default() + }) + .collect()) + } else { + Err(anyhow!("HTTP header information parsing failed!")) + } +} + +/// Returns (status_code, body_text). Separating status so the wrapper can decide on fallback. +async fn post_request_http(url: &str, body: &str, header: &str) -> ResultType<(u16, String)> { let proxy_conf = Config::get_socks(); - let tls_url = get_url_for_tls(&url, &proxy_conf); + let tls_url = get_url_for_tls(url, &proxy_conf); let tls_type = get_cached_tls_type(tls_url); let danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url); let response = post_request_( - &url, + url, tls_url, - body.clone(), + body.to_owned(), header, tls_type, danger_accept_invalid_cert, danger_accept_invalid_cert, ) .await?; - Ok(response.text().await?) + let status = response.status().as_u16(); + let text = response.text().await?; + Ok((status, text)) +} + +/// Try `http_fn` first; on connection failure or 5xx, fall back to `tcp_fn` +/// if the URL is eligible. 4xx responses are returned as-is. +async fn with_tcp_proxy_fallback( + url: &str, + method: &str, + http_fn: HttpFut, + tcp_fn: TcpFut, +) -> ResultType +where + HttpFut: Future>, + TcpFut: Future>, +{ + if should_use_raw_tcp_for_api(url) { + return tcp_fn.await; + } + + let http_result = http_fn.await; + let should_fallback = match &http_result { + Err(_) => true, + Ok((status, _)) => *status >= 500, + }; + + if should_fallback && can_fallback_to_raw_tcp(url) { + log::warn!( + "HTTP {} to {} failed or 5xx (result: {:?}), trying TCP proxy fallback", + method, + tcp_proxy_log_target(url), + http_result + .as_ref() + .map(|(s, _)| *s) + .map_err(|e| e.to_string()), + ); + match tcp_fn.await { + Ok(resp) => return Ok(resp), + Err(tcp_err) => { + log::warn!("TCP proxy fallback also failed: {:?}", tcp_err); + } + } + } + + http_result.map(|(_status, text)| text) +} + +/// POST request with raw TCP proxy support. +/// - If `USE_RAW_TCP_FOR_API` is "Y" and WS is off, goes directly through TCP proxy. +/// - Otherwise tries HTTP first; on connection failure or 5xx status, +/// falls back to TCP proxy if WS is off. +/// - 4xx responses are returned as-is (server is reachable, business logic error). +/// - If fallback also fails, returns the original HTTP result (text or error). +pub async fn post_request(url: String, body: String, header: &str) -> ResultType { + with_tcp_proxy_fallback( + &url, + "POST", + post_request_http(&url, &body, header), + post_request_via_tcp_proxy(&url, &body, header), + ) + .await } #[async_recursion] @@ -1246,21 +1511,16 @@ async fn get_http_response_async( tls_type.unwrap_or(TlsType::Rustls), danger_accept_invalid_cert.unwrap_or(false), ); - let mut http_client = match method { + let normalized_method = method.to_ascii_lowercase(); + let mut http_client = match normalized_method.as_str() { "get" => http_client.get(url), "post" => http_client.post(url), "put" => http_client.put(url), "delete" => http_client.delete(url), _ => return Err(anyhow!("The HTTP request method is not supported!")), }; - let v = serde_json::from_str(header)?; - - if let Value::Object(obj) = v { - for (key, value) in obj.iter() { - http_client = http_client.header(key, value.as_str().unwrap_or_default()); - } - } else { - return Err(anyhow!("HTTP header information parsing failed!")); + for entry in parse_json_header_entries(header)? { + http_client = http_client.header(entry.name, entry.value); } if tls_type.is_some() && danger_accept_invalid_cert.is_some() { @@ -1340,6 +1600,51 @@ async fn get_http_response_async( } } +/// Returns (status_code, json_string) so the caller can inspect the status +/// without re-parsing the serialized JSON. +async fn http_request_http( + url: &str, + method: &str, + body: Option, + header: &str, +) -> ResultType<(u16, String)> { + let proxy_conf = Config::get_socks(); + let tls_url = get_url_for_tls(url, &proxy_conf); + let tls_type = get_cached_tls_type(tls_url); + let danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url); + let response = get_http_response_async( + url, + tls_url, + method, + body, + header, + tls_type, + danger_accept_invalid_cert, + danger_accept_invalid_cert, + ) + .await?; + // Serialize response headers + let mut response_headers = Map::new(); + for (key, value) in response.headers() { + response_headers.insert(key.to_string(), json!(value.to_str().unwrap_or(""))); + } + + let status_code = response.status().as_u16(); + let response_body = response.text().await?; + + // Construct the JSON object + let mut result = Map::new(); + result.insert("status_code".to_string(), json!(status_code)); + result.insert("headers".to_string(), Value::Object(response_headers)); + result.insert("body".to_string(), json!(response_body)); + + // Convert map to JSON string + let json_str = serde_json::to_string(&result) + .map_err(|e| anyhow!("Failed to serialize response: {}", e))?; + Ok((status_code, json_str)) +} + +/// HTTP request with raw TCP proxy support. #[tokio::main(flavor = "current_thread")] pub async fn http_request_sync( url: String, @@ -1347,44 +1652,28 @@ pub async fn http_request_sync( body: Option, header: String, ) -> ResultType { - let proxy_conf = Config::get_socks(); - let tls_url = get_url_for_tls(&url, &proxy_conf); - let tls_type = get_cached_tls_type(tls_url); - let danger_accept_invalid_cert = get_cached_tls_accept_invalid_cert(tls_url); - let response = get_http_response_async( + with_tcp_proxy_fallback( &url, - tls_url, &method, - body.clone(), - &header, - tls_type, - danger_accept_invalid_cert, - danger_accept_invalid_cert, + http_request_http(&url, &method, body.clone(), &header), + http_request_via_tcp_proxy(&url, &method, body.as_deref(), &header), ) - .await?; - // Serialize response headers - let mut response_headers = serde_json::map::Map::new(); - for (key, value) in response.headers() { - response_headers.insert( - key.to_string(), - serde_json::json!(value.to_str().unwrap_or("")), - ); - } + .await +} - let status_code = response.status().as_u16(); - let response_body = response.text().await?; +/// General HTTP request via TCP proxy. Header is a JSON string (used by http_request_sync). +/// Returns a JSON string with status_code, headers, body (same format as http_request_sync). +async fn http_request_via_tcp_proxy( + url: &str, + method: &str, + body: Option<&str>, + header: &str, +) -> ResultType { + let headers = parse_json_header_entries(header)?; + let body_bytes = body.unwrap_or("").as_bytes(); - // Construct the JSON object - let mut result = serde_json::map::Map::new(); - result.insert("status_code".to_string(), serde_json::json!(status_code)); - result.insert( - "headers".to_string(), - serde_json::Value::Object(response_headers), - ); - result.insert("body".to_string(), serde_json::json!(response_body)); - - // Convert map to JSON string - serde_json::to_string(&result).map_err(|e| anyhow!("Failed to serialize response: {}", e)) + let resp = tcp_proxy_request(method, url, body_bytes, headers).await?; + http_proxy_response_to_json(resp) } #[inline] @@ -1647,7 +1936,7 @@ pub fn check_process(arg: &str, mut same_uid: bool) -> bool { false } -pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> { +async fn secure_tcp_impl(conn: &mut Stream, key: &str, log_on_success: bool) -> ResultType<()> { // Skip additional encryption when using WebSocket connections (wss://) // as WebSocket Secure (wss://) already provides transport layer encryption. // This doesn't affect the end-to-end encryption between clients, @@ -1680,7 +1969,9 @@ pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> { }); timeout(CONNECT_TIMEOUT, conn.send(&msg_out)).await??; conn.set_key(key); - log::info!("Connection secured"); + if log_on_success { + log::info!("Connection secured"); + } } _ => {} } @@ -1691,6 +1982,14 @@ pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> { Ok(()) } +pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> { + secure_tcp_impl(conn, key, true).await +} + +async fn secure_tcp_silent(conn: &mut Stream, key: &str) -> ResultType<()> { + secure_tcp_impl(conn, key, false).await +} + #[inline] fn get_pk(pk: &[u8]) -> Option<[u8; 32]> { if pk.len() == 32 { @@ -2468,11 +2767,13 @@ mod tests { assert!(is_public("https://rustdesk.com/")); assert!(is_public("https://www.rustdesk.com/")); assert!(is_public("https://api.rustdesk.com/v1")); + assert!(is_public("https://API.RUSTDESK.COM/v1")); assert!(is_public("https://rustdesk.com/path")); // Test URLs ending with "rustdesk.com" assert!(is_public("rustdesk.com")); assert!(is_public("https://rustdesk.com")); + assert!(is_public("https://RustDesk.com")); assert!(is_public("http://www.rustdesk.com")); assert!(is_public("https://api.rustdesk.com")); @@ -2485,6 +2786,193 @@ mod tests { assert!(!is_public("rustdesk.comhello.com")); } + #[test] + fn test_should_use_tcp_proxy_for_api_url() { + assert!(should_use_tcp_proxy_for_api_url( + "https://admin.example.com/api/login", + "https://admin.example.com" + )); + assert!(should_use_tcp_proxy_for_api_url( + "https://admin.example.com:21114/api/login", + "https://admin.example.com" + )); + assert!(!should_use_tcp_proxy_for_api_url( + "https://api.telegram.org/bot123/sendMessage", + "https://admin.example.com" + )); + assert!(!should_use_tcp_proxy_for_api_url( + "https://admin.rustdesk.com/api/login", + "https://admin.rustdesk.com" + )); + assert!(!should_use_tcp_proxy_for_api_url( + "https://admin.example.com/api/login", + "not a url" + )); + assert!(!should_use_tcp_proxy_for_api_url( + "not a url", + "https://admin.example.com" + )); + } + + #[test] + fn test_get_tcp_proxy_addr_normalizes_bare_ipv6_host() { + struct RestoreCustomRendezvousServer(String); + + impl Drop for RestoreCustomRendezvousServer { + fn drop(&mut self) { + Config::set_option( + keys::OPTION_CUSTOM_RENDEZVOUS_SERVER.to_string(), + self.0.clone(), + ); + } + } + + let _restore = RestoreCustomRendezvousServer(Config::get_option( + keys::OPTION_CUSTOM_RENDEZVOUS_SERVER, + )); + Config::set_option( + keys::OPTION_CUSTOM_RENDEZVOUS_SERVER.to_string(), + "1:2".to_string(), + ); + + assert_eq!(get_tcp_proxy_addr(), format!("[1:2]:{RENDEZVOUS_PORT}")); + } + + #[tokio::test] + async fn test_http_request_via_tcp_proxy_rejects_invalid_header_json() { + let result = http_request_via_tcp_proxy("not a url", "get", None, "{").await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_http_request_via_tcp_proxy_rejects_non_object_header_json() { + let err = http_request_via_tcp_proxy("not a url", "get", None, "[]") + .await + .unwrap_err() + .to_string(); + assert!(err.contains("HTTP header information parsing failed!")); + } + + #[test] + fn test_parse_json_header_entries_preserves_single_content_type() { + let headers = parse_json_header_entries( + r#"{"Content-Type":"text/plain","Authorization":"Bearer token"}"#, + ) + .unwrap(); + + assert_eq!( + headers + .iter() + .filter(|entry| entry.name.eq_ignore_ascii_case("Content-Type")) + .count(), + 1 + ); + assert_eq!( + headers + .iter() + .find(|entry| entry.name.eq_ignore_ascii_case("Content-Type")) + .map(|entry| entry.value.as_str()), + Some("text/plain") + ); + } + + #[test] + fn test_parse_json_header_entries_does_not_add_default_content_type() { + let headers = parse_json_header_entries(r#"{"Authorization":"Bearer token"}"#).unwrap(); + + assert!(!headers + .iter() + .any(|entry| entry.name.eq_ignore_ascii_case("Content-Type"))); + } + + #[test] + fn test_parse_simple_header_respects_custom_content_type() { + let headers = parse_simple_header("Content-Type: text/plain"); + + assert_eq!( + headers + .iter() + .filter(|entry| entry.name.eq_ignore_ascii_case("Content-Type")) + .count(), + 1 + ); + assert_eq!( + headers + .iter() + .find(|entry| entry.name.eq_ignore_ascii_case("Content-Type")) + .map(|entry| entry.value.as_str()), + Some("text/plain") + ); + } + + #[test] + fn test_parse_simple_header_preserves_non_content_type_header() { + let headers = parse_simple_header("Authorization: Bearer token"); + + assert!(headers.iter().any(|entry| { + entry.name.eq_ignore_ascii_case("Authorization") + && entry.value.as_str() == "Bearer token" + })); + assert_eq!( + headers + .iter() + .filter(|entry| entry.name.eq_ignore_ascii_case("Content-Type")) + .count(), + 1 + ); + assert_eq!( + headers + .iter() + .find(|entry| entry.name.eq_ignore_ascii_case("Content-Type")) + .map(|entry| entry.value.as_str()), + Some("application/json") + ); + } + + #[test] + fn test_tcp_proxy_log_target_redacts_query_only() { + assert_eq!( + tcp_proxy_log_target("https://example.com/api/heartbeat?token=secret"), + "https://example.com/api/heartbeat" + ); + } + + #[test] + fn test_tcp_proxy_log_target_brackets_ipv6_host_with_port() { + assert_eq!( + tcp_proxy_log_target("https://[2001:db8::1]:21114/api/heartbeat?token=secret"), + "https://[2001:db8::1]:21114/api/heartbeat" + ); + } + + #[test] + fn test_http_proxy_response_to_json() { + let mut resp = HttpProxyResponse { + status: 200, + body: br#"{"ok":true}"#.to_vec().into(), + ..Default::default() + }; + resp.headers.push(HeaderEntry { + name: "Content-Type".into(), + value: "application/json".into(), + ..Default::default() + }); + + let json = http_proxy_response_to_json(resp).unwrap(); + let value: Value = serde_json::from_str(&json).unwrap(); + assert_eq!(value["status_code"], 200); + assert_eq!(value["headers"]["content-type"], "application/json"); + assert_eq!(value["body"], r#"{"ok":true}"#); + + let err = http_proxy_response_to_json(HttpProxyResponse { + error: "dial failed".into(), + ..Default::default() + }) + .unwrap_err() + .to_string(); + assert!(err.contains("TCP proxy error: dial failed")); + } + #[test] fn test_mouse_event_constants_and_mask_layout() { use super::input::*; diff --git a/src/hbbs_http.rs b/src/hbbs_http.rs index 20316b6f5..9e4538697 100644 --- a/src/hbbs_http.rs +++ b/src/hbbs_http.rs @@ -1,4 +1,4 @@ -use reqwest::blocking::Response; +use hbb_common::ResultType; use serde::de::DeserializeOwned; use serde_json::{Map, Value}; @@ -21,11 +21,9 @@ pub enum HbbHttpResponse { Data(T), } -impl TryFrom for HbbHttpResponse { - type Error = reqwest::Error; - - fn try_from(resp: Response) -> Result>::Error> { - let map = resp.json::>()?; +impl HbbHttpResponse { + pub fn parse(body: &str) -> ResultType { + let map = serde_json::from_str::>(body)?; if let Some(error) = map.get("error") { if let Some(err) = error.as_str() { Ok(Self::Error(err.to_owned())) diff --git a/src/hbbs_http/account.rs b/src/hbbs_http/account.rs index 8e6141200..3f824113b 100644 --- a/src/hbbs_http/account.rs +++ b/src/hbbs_http/account.rs @@ -1,7 +1,6 @@ use super::HbbHttpResponse; use crate::hbbs_http::create_http_client_with_url; use hbb_common::{config::LocalConfig, log, ResultType}; -use reqwest::blocking::Client; use serde_derive::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; use std::{ @@ -109,7 +108,7 @@ pub struct AuthBody { } pub struct OidcSession { - client: Option, + warmed_api_server: Option, state_msg: &'static str, failed_msg: String, code_url: Option, @@ -136,7 +135,7 @@ impl Default for UserStatus { impl OidcSession { fn new() -> Self { Self { - client: None, + warmed_api_server: None, state_msg: REQUESTING_ACCOUNT_AUTH, failed_msg: "".to_owned(), code_url: None, @@ -149,12 +148,13 @@ impl OidcSession { fn ensure_client(api_server: &str) { let mut write_guard = OIDC_SESSION.write().unwrap(); - if write_guard.client.is_none() { - // This URL is used to detect the appropriate TLS implementation for the server. - let login_option_url = format!("{}/api/login-options", &api_server); - let client = create_http_client_with_url(&login_option_url); - write_guard.client = Some(client); + if write_guard.warmed_api_server.as_deref() == Some(api_server) { + return; } + // This URL is used to detect the appropriate TLS implementation for the server. + let login_option_url = format!("{}/api/login-options", api_server); + let _ = create_http_client_with_url(&login_option_url); + write_guard.warmed_api_server = Some(api_server.to_owned()); } fn auth( @@ -164,26 +164,15 @@ impl OidcSession { uuid: &str, ) -> ResultType> { Self::ensure_client(api_server); - let resp = if let Some(client) = &OIDC_SESSION.read().unwrap().client { - client - .post(format!("{}/api/oidc/auth", api_server)) - .json(&serde_json::json!({ - "op": op, - "id": id, - "uuid": uuid, - "deviceInfo": crate::ui_interface::get_login_device_info(), - })) - .send()? - } else { - hbb_common::bail!("http client not initialized"); - }; - let status = resp.status(); - match resp.try_into() { - Ok(v) => Ok(v), - Err(err) => { - hbb_common::bail!("Http status: {}, err: {}", status, err); - } - } + let body = serde_json::json!({ + "op": op, + "id": id, + "uuid": uuid, + "deviceInfo": crate::ui_interface::get_login_device_info(), + }) + .to_string(); + let resp = crate::post_request_sync(format!("{}/api/oidc/auth", api_server), body, "")?; + HbbHttpResponse::parse(&resp) } fn query( @@ -197,11 +186,19 @@ impl OidcSession { &[("code", code), ("id", id), ("uuid", uuid)], )?; Self::ensure_client(api_server); - if let Some(client) = &OIDC_SESSION.read().unwrap().client { - Ok(client.get(url).send()?.try_into()?) - } else { - hbb_common::bail!("http client not initialized") + #[derive(Deserialize)] + struct HttpResponseBody { + body: String, } + + let resp = crate::http_request_sync( + url.to_string(), + "GET".to_owned(), + None, + "{}".to_owned(), + )?; + let resp = serde_json::from_str::(&resp)?; + HbbHttpResponse::parse(&resp.body) } fn reset(&mut self) { From 9cf1338dc41e569d3a99b5ef3dfd057743d3c2a6 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Sat, 4 Apr 2026 22:54:13 +0800 Subject: [PATCH 200/277] fix(win): exe icon path (#14686) * fix(win): exe icon path Signed-off-by: fufesou * fix(win): Simple refactor Signed-off-by: fufesou --------- Signed-off-by: fufesou --- src/platform/windows.rs | 44 +++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 7e4e390aa..4c09bbe9f 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -1472,7 +1472,7 @@ pub fn install_me(options: &str, path: String, silent: bool, debug: bool) -> Res let tmp_path = std::env::temp_dir().to_string_lossy().to_string(); let cur_exe = current_exe.to_str().unwrap_or("").to_owned(); - let shortcut_icon_location = get_shortcut_icon_location(&cur_exe); + let shortcut_icon_location = get_shortcut_icon_location(&path, &cur_exe); let mk_shortcut = write_cmds( format!( " @@ -1510,7 +1510,7 @@ oLink.Save .to_str() .unwrap_or("") .to_owned(); - let tray_shortcut = get_tray_shortcut(&exe, &tmp_path)?; + let tray_shortcut = get_tray_shortcut(&path, &exe, &cur_exe, &tmp_path)?; let mut reg_value_desktop_shortcuts = "0".to_owned(); let mut reg_value_start_menu_shortcuts = "0".to_owned(); let mut reg_value_printer = "0".to_owned(); @@ -1621,7 +1621,7 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\" {install_remote_printer} {sleep} ", - display_icon = get_custom_icon(&cur_exe).unwrap_or(exe.to_string()), + display_icon = get_custom_icon(&path, &cur_exe).unwrap_or(exe.to_string()), version = crate::VERSION.replace("-", "."), build_date = crate::BUILD_DATE, after_install = get_after_install( @@ -2125,12 +2125,16 @@ unsafe fn set_default_dll_directories() -> bool { true } -fn get_custom_icon(exe: &str) -> Option { +fn get_custom_icon(install_dir: &str, exe: &str) -> Option { + const RELATIVE_ICON_PATH: &str = "data\\flutter_assets\\assets\\icon.ico"; if crate::is_custom_client() { if let Some(p) = PathBuf::from(exe).parent() { - let alter_icon_path = p.join("data\\flutter_assets\\assets\\icon.ico"); + let alter_icon_path = p.join(RELATIVE_ICON_PATH); if alter_icon_path.exists() { - // Verify that the icon is not a symlink for security + // During installation, files under `install_dir` may not exist yet. + // So we validate the icon from the current executable directory first. + // But for shortcut/registry icon location, we should point to the final + // installed path so the icon works across different Windows users. if let Ok(metadata) = std::fs::symlink_metadata(&alter_icon_path) { if metadata.is_symlink() { log::warn!( @@ -2140,7 +2144,11 @@ fn get_custom_icon(exe: &str) -> Option { return None; } if metadata.is_file() { - return Some(alter_icon_path.to_string_lossy().to_string()); + return if install_dir.is_empty() { + Some(alter_icon_path.to_string_lossy().to_string()) + } else { + Some(format!("{}\\{}", install_dir, RELATIVE_ICON_PATH)) + }; } } } @@ -2150,12 +2158,12 @@ fn get_custom_icon(exe: &str) -> Option { } #[inline] -fn get_shortcut_icon_location(exe: &str) -> String { +fn get_shortcut_icon_location(install_dir: &str, exe: &str) -> String { if exe.is_empty() { return "".to_owned(); } - get_custom_icon(exe) + get_custom_icon(install_dir, exe) .map(|p| format!("oLink.IconLocation = \"{}\"", p)) .unwrap_or_default() } @@ -2166,7 +2174,7 @@ pub fn create_shortcut(id: &str) -> ResultType<()> { // Replace ':' with '_' for filename since ':' is not allowed in Windows filenames // https://github.com/rustdesk/hbb_common/blob/8b0e25867375ba9e6bff548acf44fe6d6ffa7c0e/src/config.rs#L1384 let filename = id.replace(':', "_"); - let shortcut_icon_location = get_shortcut_icon_location(&exe); + let shortcut_icon_location = get_shortcut_icon_location("", &exe); let shortcut = write_cmds( format!( " @@ -2953,9 +2961,9 @@ pub fn uninstall_service(show_new_window: bool, _: bool) -> bool { pub fn install_service() -> bool { log::info!("Installing service..."); let _installing = crate::platform::InstallingService::new(); - let (_, _, _, exe) = get_install_info(); + let (_, path, _, exe) = get_install_info(); let tmp_path = std::env::temp_dir().to_string_lossy().to_string(); - let tray_shortcut = get_tray_shortcut(&exe, &tmp_path).unwrap_or_default(); + let tray_shortcut = get_tray_shortcut(&path, &exe, &exe, &tmp_path).unwrap_or_default(); let filter = format!(" /FI \"PID ne {}\"", get_current_pid()); Config::set_option("stop-service".into(), "".into()); crate::ipc::EXIT_RECV_CLOSE.store(false, Ordering::Relaxed); @@ -3064,7 +3072,8 @@ pub fn update_me(debug: bool) -> ResultType<()> { let version = crate::VERSION.replace("-", "."); let size = get_directory_size_kb(&path); let build_date = crate::BUILD_DATE; - let display_icon = get_custom_icon(&exe).unwrap_or(exe.to_string()); + // Use the icon in the previous installation directory if possible. + let display_icon = get_custom_icon("", &exe).unwrap_or(exe.to_string()); let is_msi = is_msi_installed().ok(); @@ -3421,8 +3430,13 @@ pub fn update_me_msi(msi: &str, quiet: bool) -> ResultType<()> { Ok(()) } -pub fn get_tray_shortcut(exe: &str, tmp_path: &str) -> ResultType { - let shortcut_icon_location = get_shortcut_icon_location(exe); +pub fn get_tray_shortcut( + install_dir: &str, + exe: &str, + icon_source_exe: &str, + tmp_path: &str, +) -> ResultType { + let shortcut_icon_location = get_shortcut_icon_location(install_dir, icon_source_exe); Ok(write_cmds( format!( " From e0427bdc772756d1f740008aba983e9a206fdaec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=8C=90=20Qusai=20ALBahri=20=F0=9F=8C=B1?= <115154006+QusaiALBahri@users.noreply.github.com> Date: Mon, 6 Apr 2026 13:27:14 +0300 Subject: [PATCH 201/277] Translate UI strings to Arabic in ar.rs (#14694) --- src/lang/ar.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 8204da6fd..6d48e34ee 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -729,19 +729,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server-oss-not-support-tip", "هذه الميزة غير مدعومة من قبل خادمك"), ("input note here", "أدخل الملاحظة هنا"), ("note-at-conn-end-tip", "سيتم عرض هذه الملاحظة عند نهاية الاتصال"), - ("Show terminal extra keys", ""), - ("Relative mouse mode", ""), - ("rel-mouse-not-supported-peer-tip", ""), - ("rel-mouse-not-ready-tip", ""), - ("rel-mouse-lock-failed-tip", ""), - ("rel-mouse-exit-{}-tip", ""), - ("rel-mouse-permission-lost-tip", ""), - ("Changelog", ""), - ("keep-awake-during-outgoing-sessions-label", ""), - ("keep-awake-during-incoming-sessions-label", ""), + ("Show terminal extra keys", "إظهار مفاتيح إضافية في الطرفية"), + ("Relative mouse mode", "وضع الماوس النسبي"), + ("rel-mouse-not-supported-peer-tip", "وضع الماوس النسبي غير مدعوم على الجهاز الآخر"), + ("rel-mouse-not-ready-tip", "وضع الماوس النسبي غير جاهز"), + ("rel-mouse-lock-failed-tip", "فشل قفل الماوس النسبي"), + ("rel-mouse-exit-{}-tip", "للخروج من وضع الماوس النسبي اضغط على {}"), + ("rel-mouse-permission-lost-tip", "تم فقدان إذن الماوس النسبي"), + ("Changelog", "سجل التغييرات"), + ("keep-awake-during-outgoing-sessions-label", "إبقاء الجهاز نشطًا أثناء الجلسات الصادرة"), + ("keep-awake-during-incoming-sessions-label", "إبقاء الجهاز نشطًا أثناء الجلسات الواردة"), ("Continue with {}", "متابعة مع {}"), - ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), + ("Display Name", "اسم العرض"), + ("password-hidden-tip", "كلمة المرور مخفية"), + ("preset-password-in-use-tip", "كلمة المرور المحددة مسبقًا قيد الاستخدام"), ].iter().cloned().collect(); } From 9d3bc7d9e6e65db3f9931423e76129075d83342b Mon Sep 17 00:00:00 2001 From: 21pages Date: Tue, 7 Apr 2026 23:39:24 +0800 Subject: [PATCH 202/277] fix switch sides for macOS peers (#14661) Signed-off-by: 21pages --- flutter/lib/common/widgets/toolbar.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index a46ce54fd..1a6160324 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -275,7 +275,6 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { isDesktop && ffiModel.keyboard && pi.platform != kPeerPlatformAndroid && - pi.platform != kPeerPlatformMacOS && versionCmp(pi.version, '1.2.0') >= 0 && bind.peerGetSessionsCount(id: id, connType: ffi.connType.index) == 1) { v.add(TTextMenu( From 0cf3e8ed40ad9f305cdac2eda933ebb40a8dda92 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Thu, 9 Apr 2026 15:12:57 +0800 Subject: [PATCH 203/277] improve agent md --- AGENTS.md | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 92 +---------------------------------------------- GEMINI.md | 1 + 3 files changed, 108 insertions(+), 91 deletions(-) create mode 100644 AGENTS.md create mode 100644 GEMINI.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..68526d66d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,106 @@ +# RustDesk Guide + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Development Commands + +### Build Commands +- `cargo run` - Build and run the desktop application (requires libsciter library) +- `python3 build.py --flutter` - Build Flutter version (desktop) +- `python3 build.py --flutter --release` - Build Flutter version in release mode +- `python3 build.py --hwcodec` - Build with hardware codec support +- `python3 build.py --vram` - Build with VRAM feature (Windows only) +- `cargo build --release` - Build Rust binary in release mode +- `cargo build --features hwcodec` - Build with specific features + +### Flutter Mobile Commands +- `cd flutter && flutter build android` - Build Android APK +- `cd flutter && flutter build ios` - Build iOS app +- `cd flutter && flutter run` - Run Flutter app in development mode +- `cd flutter && flutter test` - Run Flutter tests + +### Testing +- `cargo test` - Run Rust tests +- `cd flutter && flutter test` - Run Flutter tests + +### Platform-Specific Build Scripts +- `flutter/build_android.sh` - Android build script +- `flutter/build_ios.sh` - iOS build script +- `flutter/build_fdroid.sh` - F-Droid build script + +## Project Architecture + +### Directory Structure +- **`src/`** - Main Rust application code + - `src/ui/` - Legacy Sciter UI (deprecated, use Flutter instead) + - `src/server/` - Audio/clipboard/input/video services and network connections + - `src/client.rs` - Peer connection handling + - `src/platform/` - Platform-specific code +- **`flutter/`** - Flutter UI code for desktop and mobile +- **`libs/`** - Core libraries + - `libs/hbb_common/` - Video codec, config, network wrapper, protobuf, file transfer utilities + - `libs/scrap/` - Screen capture functionality + - `libs/enigo/` - Platform-specific keyboard/mouse control + - `libs/clipboard/` - Cross-platform clipboard implementation + +### Key Components +- **Remote Desktop Protocol**: Custom protocol implemented in `src/rendezvous_mediator.rs` for communicating with rustdesk-server +- **Screen Capture**: Platform-specific screen capture in `libs/scrap/` +- **Input Handling**: Cross-platform input simulation in `libs/enigo/` +- **Audio/Video Services**: Real-time audio/video streaming in `src/server/` +- **File Transfer**: Secure file transfer implementation in `libs/hbb_common/` + +### UI Architecture +- **Legacy UI**: Sciter-based (deprecated) - files in `src/ui/` +- **Modern UI**: Flutter-based - files in `flutter/` + - Desktop: `flutter/lib/desktop/` + - Mobile: `flutter/lib/mobile/` + - Shared: `flutter/lib/common/` and `flutter/lib/models/` + +## Important Build Notes + +### Dependencies +- Requires vcpkg for C++ dependencies: `libvpx`, `libyuv`, `opus`, `aom` +- Set `VCPKG_ROOT` environment variable +- Download appropriate Sciter library for legacy UI support + +### Ignore Patterns +When working with files, ignore these directories: +- `target/` - Rust build artifacts +- `flutter/build/` - Flutter build output +- `flutter/.dart_tool/` - Flutter tooling files + +### Cross-Platform Considerations +- Windows builds require additional DLLs and virtual display drivers +- macOS builds need proper signing and notarization for distribution +- Linux builds support multiple package formats (deb, rpm, AppImage) +- Mobile builds require platform-specific toolchains (Android SDK, Xcode) + +### Feature Flags +- `hwcodec` - Hardware video encoding/decoding +- `vram` - VRAM optimization (Windows only) +- `flutter` - Enable Flutter UI +- `unix-file-copy-paste` - Unix file clipboard support +- `screencapturekit` - macOS ScreenCaptureKit (macOS only) + +### Config +All configurations or options are under `libs/hbb_common/src/config.rs` file, 4 types: +- Settings +- Local +- Display +- Built-in + +## Rust Rules + +- In Rust code, do not introduce `unwrap()` or `expect()`. +- Allowed exceptions: +- Tests may use `unwrap()` or `expect()` when it keeps the test focused and readable. +- Lock acquisition may use `unwrap()` only when the locking API makes that the practical option and the failure mode is poison handling rather than normal control flow. +- Outside those exceptions, propagate errors, handle them explicitly, or use safer fallbacks instead of `unwrap()` and `expect()`. + +## Editing Hygiene + +- Do not introduce formatting-only changes. +- Do not run repository-wide formatters or reflow unrelated code unless the + user explicitly asks for formatting. +- Keep diffs limited to semantic changes required for the task. diff --git a/CLAUDE.md b/CLAUDE.md index 8d46e1fa1..c31706425 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,91 +1 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Development Commands - -### Build Commands -- `cargo run` - Build and run the desktop application (requires libsciter library) -- `python3 build.py --flutter` - Build Flutter version (desktop) -- `python3 build.py --flutter --release` - Build Flutter version in release mode -- `python3 build.py --hwcodec` - Build with hardware codec support -- `python3 build.py --vram` - Build with VRAM feature (Windows only) -- `cargo build --release` - Build Rust binary in release mode -- `cargo build --features hwcodec` - Build with specific features - -### Flutter Mobile Commands -- `cd flutter && flutter build android` - Build Android APK -- `cd flutter && flutter build ios` - Build iOS app -- `cd flutter && flutter run` - Run Flutter app in development mode -- `cd flutter && flutter test` - Run Flutter tests - -### Testing -- `cargo test` - Run Rust tests -- `cd flutter && flutter test` - Run Flutter tests - -### Platform-Specific Build Scripts -- `flutter/build_android.sh` - Android build script -- `flutter/build_ios.sh` - iOS build script -- `flutter/build_fdroid.sh` - F-Droid build script - -## Project Architecture - -### Directory Structure -- **`src/`** - Main Rust application code - - `src/ui/` - Legacy Sciter UI (deprecated, use Flutter instead) - - `src/server/` - Audio/clipboard/input/video services and network connections - - `src/client.rs` - Peer connection handling - - `src/platform/` - Platform-specific code -- **`flutter/`** - Flutter UI code for desktop and mobile -- **`libs/`** - Core libraries - - `libs/hbb_common/` - Video codec, config, network wrapper, protobuf, file transfer utilities - - `libs/scrap/` - Screen capture functionality - - `libs/enigo/` - Platform-specific keyboard/mouse control - - `libs/clipboard/` - Cross-platform clipboard implementation - -### Key Components -- **Remote Desktop Protocol**: Custom protocol implemented in `src/rendezvous_mediator.rs` for communicating with rustdesk-server -- **Screen Capture**: Platform-specific screen capture in `libs/scrap/` -- **Input Handling**: Cross-platform input simulation in `libs/enigo/` -- **Audio/Video Services**: Real-time audio/video streaming in `src/server/` -- **File Transfer**: Secure file transfer implementation in `libs/hbb_common/` - -### UI Architecture -- **Legacy UI**: Sciter-based (deprecated) - files in `src/ui/` -- **Modern UI**: Flutter-based - files in `flutter/` - - Desktop: `flutter/lib/desktop/` - - Mobile: `flutter/lib/mobile/` - - Shared: `flutter/lib/common/` and `flutter/lib/models/` - -## Important Build Notes - -### Dependencies -- Requires vcpkg for C++ dependencies: `libvpx`, `libyuv`, `opus`, `aom` -- Set `VCPKG_ROOT` environment variable -- Download appropriate Sciter library for legacy UI support - -### Ignore Patterns -When working with files, ignore these directories: -- `target/` - Rust build artifacts -- `flutter/build/` - Flutter build output -- `flutter/.dart_tool/` - Flutter tooling files - -### Cross-Platform Considerations -- Windows builds require additional DLLs and virtual display drivers -- macOS builds need proper signing and notarization for distribution -- Linux builds support multiple package formats (deb, rpm, AppImage) -- Mobile builds require platform-specific toolchains (Android SDK, Xcode) - -### Feature Flags -- `hwcodec` - Hardware video encoding/decoding -- `vram` - VRAM optimization (Windows only) -- `flutter` - Enable Flutter UI -- `unix-file-copy-paste` - Unix file clipboard support -- `screencapturekit` - macOS ScreenCaptureKit (macOS only) - -### Config -All configurations or options are under `libs/hbb_common/src/config.rs` file, 4 types: -- Settings -- Local -- Display -- Built-in +AGENTS.md diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 000000000..c31706425 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1 @@ +AGENTS.md From 8dea347a216f6b34a5b53f67e5ac93eba3d731f7 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 9 Apr 2026 17:14:21 +0800 Subject: [PATCH 204/277] add brute-force protection for one-time password (#14682) * add brute-force protection for temporary password Rotate the temporary password after repeated failed login attempts within one minute, and reset the failure window after successful authentication. Signed-off-by: 21pages * replace LazyLock with lazy_static Signed-off-by: 21pages * read temporary password after locking failure state Signed-off-by: 21pages * server: rotate temporary passwords after 10 consecutive failures Signed-off-by: 21pages * server: clarify temporary password failure counter comment Signed-off-by: 21pages --------- Signed-off-by: 21pages --- src/server/connection.rs | 61 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 6 deletions(-) diff --git a/src/server/connection.rs b/src/server/connection.rs index 0e7f26263..8b4eb0c48 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1993,11 +1993,6 @@ impl Connection { constant_time_eq(&hasher2.finalize()[..], &self.lr.password[..]) } - #[inline] - fn validate_one_password(&self, password: &str) -> bool { - self.validate_password_plain(password) - } - fn validate_password_plain(&self, password: &str) -> bool { if password.is_empty() { return false; @@ -2025,15 +2020,68 @@ impl Connection { self.validate_password_plain(storage) } + // This is coarse brute-force protection for the current temporary password value. + // We only care whether the active temporary password itself was presented correctly, + // not whether later authorization steps succeed. A successful temporary-password + // match clears this state immediately, and the counter also resets whenever the + // temporary password changes or is rotated. + fn check_update_temporary_password(&self, temporary_password_success: bool) { + const MAX_CONSECUTIVE_FAILURES: i32 = 10; + #[derive(Default)] + struct State { + password: String, + failures: i32, + } + lazy_static::lazy_static! { + static ref TEMPORARY_PASSWORD_FAILURES: Mutex = + Mutex::new(State::default()); + } + + if !password::temporary_enabled() { + return; + } + + let mut state = TEMPORARY_PASSWORD_FAILURES.lock().unwrap(); + let current_password = password::temporary_password(); + if current_password.is_empty() { + return; + } + if state.password != current_password { + state.password = current_password; + state.failures = 0; + } + + if temporary_password_success { + state.failures = 0; + return; + } + state.failures += 1; + + if state.failures < MAX_CONSECUTIVE_FAILURES { + return; + } + + password::update_temporary_password(); + let new_password = password::temporary_password(); + log::warn!( + "Temporary password rotated after too many consecutive wrong attempts: failures={}, ip={}", + state.failures, + self.ip, + ); + state.password = new_password; + state.failures = 0; + } + fn validate_password(&mut self, allow_permanent_password: bool) -> bool { if password::temporary_enabled() { let password = password::temporary_password(); - if self.validate_one_password(&password) { + if self.validate_password_plain(&password) { raii::AuthedConnID::update_or_insert_session( self.session_key(), Some(password), Some(false), ); + self.check_update_temporary_password(true); return true; } } @@ -2406,6 +2454,7 @@ impl Connection { } if !self.validate_password(allow_logon_screen_password) { self.update_failure(failure, false, 0); + self.check_update_temporary_password(false); if err_msg.is_empty() { self.send_login_error(crate::client::LOGIN_MSG_PASSWORD_WRONG) .await; From 2f694c0eb2b40774285447498bee6926837602ac Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 10 Apr 2026 18:00:11 +0800 Subject: [PATCH 205/277] fix: file transfer, path traversal (#14678) * fix: file transfer, path traversal Signed-off-by: fufesou * fix(fs): remove stale files Signed-off-by: fufesou * fix(fs): update_folder_files() after set_files() Signed-off-by: fufesou * fix(fs): reduce .clone() Signed-off-by: fufesou * fix(fs): undo checking "done message for unkown id" Signed-off-by: fufesou * fix(fs): refactor 1. Hide `files` in `new_write()`. 2. Use `set_files()` to validate `files` before writing. Signed-off-by: fufesou * fix(fs): comments Signed-off-by: fufesou * fix(fs): Remove redundant checks Signed-off-by: fufesou * fix(fs): update hbb_common Signed-off-by: fufesou --------- Signed-off-by: fufesou --- libs/hbb_common | 2 +- src/client/io_loop.rs | 69 ++++++++++++++++------ src/ui_cm_interface.rs | 129 ++--------------------------------------- 3 files changed, 57 insertions(+), 143 deletions(-) diff --git a/libs/hbb_common b/libs/hbb_common index f08ce5d6d..618922b2a 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit f08ce5d6d07cd200713418ce2932769d14ff21d2 +Subproject commit 618922b2a77f7be44fc7b86e41f6cfba87d62193 diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index e0b3fcd6d..78d9a4e40 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -586,7 +586,6 @@ impl Remote { file_num, include_hidden, is_remote, - Vec::new(), od, )); allow_err!( @@ -659,7 +658,6 @@ impl Remote { file_num, include_hidden, is_remote, - Vec::new(), od, ); job.is_last_job = true; @@ -845,19 +843,7 @@ impl Remote { } } Data::CancelJob(id) => { - let mut msg_out = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_cancel(FileTransferCancel { - id: id, - ..Default::default() - }); - msg_out.set_file_action(file_action); - allow_err!(peer.send(&msg_out).await); - if let Some(job) = fs::remove_job(id, &mut self.write_jobs) { - job.remove_download_file(); - } - let _ = fs::remove_job(id, &mut self.read_jobs); - self.remove_jobs.remove(&id); + self.cancel_transfer_job(id, peer).await; } Data::RemoveDir((id, path)) => { let mut msg_out = Message::new(); @@ -1053,6 +1039,22 @@ impl Remote { } } + async fn cancel_transfer_job(&mut self, id: i32, peer: &mut Stream) { + let mut msg_out = Message::new(); + let mut file_action = FileAction::new(); + file_action.set_cancel(FileTransferCancel { + id, + ..Default::default() + }); + msg_out.set_file_action(file_action); + allow_err!(peer.send(&msg_out).await); + if let Some(job) = fs::remove_job(id, &mut self.write_jobs) { + job.remove_download_file(); + } + let _ = fs::remove_job(id, &mut self.read_jobs); + self.remove_jobs.remove(&id); + } + pub async fn sync_jobs_status_to_local(&mut self) -> bool { if !self.is_connected { return false; @@ -1470,14 +1472,43 @@ impl Remote { fs::transform_windows_path(&mut entries); } } - self.handler - .update_folder_files(fd.id, &entries, fd.path, false, false); + // We cannot call cancel_transfer_job/handle_job_status while holding + // a mutable borrow from fs::get_job(&mut self.write_jobs), so defer + // the error handling until after the borrow scope ends. + let mut set_files_err = None; if let Some(job) = fs::get_job(fd.id, &mut self.write_jobs) { log::info!("job set_files: {:?}", entries); - job.set_files(entries); - job.set_finished_size_on_resume(); + if let Err(err) = job.set_files(entries) { + set_files_err = Some(err.to_string()); + } else { + job.set_finished_size_on_resume(); + self.handler.update_folder_files( + fd.id, + job.files(), + fd.path, + false, + false, + ); + } } else if let Some(job) = self.remove_jobs.get_mut(&fd.id) { + // Intentionally keep raw entries here: + // - remote remove flow executes deletions on peer side; + // - local remove flow is populated from local get_recursive_files(). job.files = entries; + self.handler + .update_folder_files(fd.id, &job.files, fd.path, false, false); + } else { + self.handler + .update_folder_files(fd.id, &entries, fd.path, false, false); + } + if let Some(err) = set_files_err { + log::warn!( + "Rejected unsafe file list from remote peer for job {}: {}", + fd.id, + err + ); + self.cancel_transfer_job(fd.id, peer).await; + self.handle_job_status(fd.id, -1, Some(err)); } } Some(file_response::Union::Digest(digest)) => { diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 75e724007..19a9e74e7 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -941,15 +941,6 @@ async fn handle_fs( total_size, conn_id, } => { - // Validate file names to prevent path traversal attacks. - // This must be done BEFORE any path operations to ensure attackers cannot - // escape the target directory using names like "../../malicious.txt" - if let Err(e) = validate_transfer_file_names(&files) { - log::warn!("Path traversal attempt detected for {}: {}", path, e); - send_raw(fs::new_error(id, e, file_num), tx); - return; - } - // Convert files to FileEntry let file_entries: Vec = files .drain(..) @@ -970,9 +961,13 @@ async fn handle_fs( file_num, false, false, - file_entries, overwrite_detection, ); + if let Err(e) = job.set_files(file_entries) { + log::warn!("Reject unsafe transfer file list for {}: {}", path, e); + send_raw(fs::new_error(id, e, file_num), tx); + return; + } job.total_size = total_size; job.conn_id = conn_id; write_jobs.push(job); @@ -1160,73 +1155,6 @@ async fn handle_fs( } } -/// Validates that a file name does not contain path traversal sequences. -/// This prevents attackers from escaping the base directory by using names like -/// "../../../etc/passwd" or "..\\..\\Windows\\System32\\malicious.dll". -#[cfg(not(any(target_os = "ios")))] -fn validate_file_name_no_traversal(name: &str) -> ResultType<()> { - // Check for null bytes which could cause path truncation in some APIs - if name.bytes().any(|b| b == 0) { - bail!("file name contains null bytes"); - } - - // Check for path traversal patterns - // We check for both Unix and Windows path separators - if name - .split(|c| c == '/' || c == '\\') - .filter(|s| !s.is_empty()) - .any(|component| component == "..") - { - bail!("path traversal detected in file name"); - } - - // On Windows, also check for drive letters (e.g., "C:") - #[cfg(windows)] - { - if name.len() >= 2 { - let bytes = name.as_bytes(); - if bytes[0].is_ascii_alphabetic() && bytes[1] == b':' { - bail!("absolute path detected in file name"); - } - } - } - - // Check for names starting with path separator: - // - Unix absolute paths (e.g., "/etc/passwd") - // - Windows UNC paths (e.g., "\\server\share") - if name.starts_with('/') || name.starts_with('\\') { - bail!("absolute path detected in file name"); - } - - Ok(()) -} - -#[inline] -fn is_single_file_with_empty_name(files: &[(String, u64)]) -> bool { - files.len() == 1 && files.first().map_or(false, |f| f.0.is_empty()) -} - -/// Validates all file names in a transfer request to prevent path traversal attacks. -/// Returns an error if any file name contains dangerous path components. -#[cfg(not(any(target_os = "ios")))] -fn validate_transfer_file_names(files: &[(String, u64)]) -> ResultType<()> { - if is_single_file_with_empty_name(files) { - // Allow empty name for single file. - // The full path is provided in the `path` parameter for single file transfers. - return Ok(()); - } - - for (name, _) in files { - // In multi-file transfers, empty names are not allowed. - // Each file must have a valid name to construct the destination path. - if name.is_empty() { - bail!("empty file name in multi-file transfer"); - } - validate_file_name_no_traversal(name)?; - } - Ok(()) -} - /// Start a read job in CM for file transfer from server to client (Windows only). /// /// This creates a `TransferJob` using `new_read()`, validates it, and sends the @@ -1601,16 +1529,7 @@ async fn create_dir(path: String, id: i32, tx: &UnboundedSender) { #[cfg(not(any(target_os = "ios")))] async fn rename_file(path: String, new_name: String, id: i32, tx: &UnboundedSender) { handle_result( - spawn_blocking(move || { - // Rename target must not be empty - if new_name.is_empty() { - bail!("new file name cannot be empty"); - } - // Validate that new_name doesn't contain path traversal - validate_file_name_no_traversal(&new_name)?; - fs::rename_file(&path, &new_name) - }) - .await, + spawn_blocking(move || fs::rename_file(&path, &new_name)).await, id, 0, tx, @@ -1773,42 +1692,6 @@ mod tests { }); } - #[test] - #[cfg(not(any(target_os = "ios")))] - fn validate_file_name_security() { - // Null byte injection - assert!(super::validate_file_name_no_traversal("file\0.txt").is_err()); - assert!(super::validate_file_name_no_traversal("test\0").is_err()); - - // Path traversal - assert!(super::validate_file_name_no_traversal("../etc/passwd").is_err()); - assert!(super::validate_file_name_no_traversal("foo/../bar").is_err()); - assert!(super::validate_file_name_no_traversal("..").is_err()); - - // Absolute paths - assert!(super::validate_file_name_no_traversal("/etc/passwd").is_err()); - assert!(super::validate_file_name_no_traversal("\\Windows").is_err()); - #[cfg(windows)] - assert!(super::validate_file_name_no_traversal("C:\\Windows").is_err()); - - // Valid paths - assert!(super::validate_file_name_no_traversal("file.txt").is_ok()); - assert!(super::validate_file_name_no_traversal("subdir/file.txt").is_ok()); - assert!(super::validate_file_name_no_traversal("").is_ok()); - } - - #[test] - #[cfg(not(any(target_os = "ios")))] - fn validate_transfer_file_names_security() { - assert!(super::validate_transfer_file_names(&[("file.txt".into(), 100)]).is_ok()); - assert!(super::validate_transfer_file_names(&[("".into(), 100)]).is_ok()); - assert!( - super::validate_transfer_file_names(&[("".into(), 100), ("file.txt".into(), 100)]) - .is_err() - ); - assert!(super::validate_transfer_file_names(&[("../passwd".into(), 100)]).is_err()); - } - /// Tests that symlink creation works on this platform. /// This is a helper to verify the test environment supports symlinks. #[test] From 771cb4ebd73c8325f4784d4a64a65614ab6200c6 Mon Sep 17 00:00:00 2001 From: Leo Louis Date: Mon, 13 Apr 2026 10:33:35 +0530 Subject: [PATCH 206/277] Update capture function return type for PixelProvider (#14747) --- libs/scrap/src/wayland/pipewire.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index d29677c7a..aedf786b7 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -346,7 +346,7 @@ impl PipeWireRecorder { } impl Recorder for PipeWireRecorder { - fn capture(&mut self, timeout_ms: u64) -> Result> { + fn capture(&mut self, timeout_ms: u64) -> Result, Box> { if let Some(sample) = self .appsink .try_pull_sample(gst::ClockTime::from_mseconds(timeout_ms)) From a8dc6fc632ce80c7b6d7b3a47f3782ff556fe9d9 Mon Sep 17 00:00:00 2001 From: Leo Louis Date: Mon, 13 Apr 2026 10:34:43 +0530 Subject: [PATCH 207/277] Fix capture method return type in Recorder trait (#14748) --- libs/scrap/src/wayland/capturable.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/scrap/src/wayland/capturable.rs b/libs/scrap/src/wayland/capturable.rs index 61f80ecbf..070b66799 100644 --- a/libs/scrap/src/wayland/capturable.rs +++ b/libs/scrap/src/wayland/capturable.rs @@ -24,7 +24,7 @@ impl<'a> PixelProvider<'a> { } pub trait Recorder { - fn capture(&mut self, timeout_ms: u64) -> Result>; + fn capture(&mut self, timeout_ms: u64) -> Result, Box>; } pub trait BoxCloneCapturable { From ffd2d26c1a39308d4209be7b7f5d2ac845f15426 Mon Sep 17 00:00:00 2001 From: Andrzej Rudnik Date: Tue, 14 Apr 2026 08:20:35 +0200 Subject: [PATCH 208/277] Update pl.rs (#14775) --- src/lang/pl.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 51611c9b3..2000de2c8 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -379,7 +379,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Share", "Udostępnianie ekranu"), ("ubuntu-21-04-required", "Wayland wymaga Ubuntu 21.04 lub nowszego."), ("wayland-requires-higher-linux-version", "Wayland wymaga nowszej dystrybucji Linuksa. Wypróbuj pulpit X11 lub zmień system operacyjny."), - ("xdp-portal-unavailable", ""), + ("xdp-portal-unavailable", "Nie udało się przechwycić ekranu Wayland. Portal XDG Desktop mógł ulec awarii lub jest niedostępny. Spróbuj go ponownie uruchomić poleceniem `systemctl --user restart xdg-desktop-portal`."), ("JumpLink", "Podgląd"), ("Please Select the screen to be shared(Operate on the peer side).", "Wybierz ekran do udostępnienia (działaj po zdalnego urządzenia)."), ("Show RustDesk", "Pokaż RustDesk"), @@ -740,8 +740,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", "Utrzymuj urządzenie w stanie aktywnym podczas sesji wychodzących"), ("keep-awake-during-incoming-sessions-label", "Utrzymuj urządzenie w stanie aktywnym podczas sesji przychodzących"), ("Continue with {}", "Kontynuuj z {}"), - ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), + ("Display Name", "Nazwa wyświetlana"), + ("password-hidden-tip", "Ustawiono (ukryto) stare hasło."), + ("preset-password-in-use-tip", "Obecnie używane jest hasło domyślne."), ].iter().cloned().collect(); } From 2d41b3e80dedc1d88d0409dfcd6dbb446537226d Mon Sep 17 00:00:00 2001 From: Leo Louis Date: Tue, 14 Apr 2026 11:51:10 +0530 Subject: [PATCH 209/277] Add Gujarati language support with translations (#14752) --- src/lang/gu.rs | 746 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 746 insertions(+) create mode 100644 src/lang/gu.rs diff --git a/src/lang/gu.rs b/src/lang/gu.rs new file mode 100644 index 000000000..39c45597c --- /dev/null +++ b/src/lang/gu.rs @@ -0,0 +1,746 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "સ્થિતિ"), + ("Your Desktop", "તમારું ડેસ્કટોપ"), + ("desk_tip", "તમારું ડેસ્કટોપ આ ID અને પાસવર્ડ દ્વારા એક્સેસ કરી શકાય છે."), + ("Password", "પાસવર્ડ"), + ("Ready", "તૈયાર"), + ("Established", "સ્થાપિત"), + ("connecting_status", "નેટવર્ક સાથે જોડાઈ રહ્યું છે..."), + ("Enable service", "સેવા સક્ષમ કરો"), + ("Start service", "સેવા શરૂ કરો"), + ("Service is running", "સેવા કાર્યરત છે"), + ("Service is not running", "સેવા કાર્યરત નથી"), + ("not_ready_status", "તૈયાર નથી. કૃપા કરીને તમારું કનેક્શન તપાસો"), + ("Control Remote Desktop", "રિમોટ ડેસ્કટોપ નિયંત્રિત કરો"), + ("Transfer file", "ફાઇલ ટ્રાન્સફર"), + ("Connect", "કનેક્ટ કરો"), + ("Recent sessions", "તાજેતરના સત્રો"), + ("Address book", "એડ્રેસ બુક"), + ("Confirmation", "પુષ્ટિકરણ"), + ("TCP tunneling", "TCP ટનલિંગ"), + ("Remove", "દૂર કરો"), + ("Refresh random password", "રેન્ડમ પાસવર્ડ બદલો"), + ("Set your own password", "તમારો પોતાનો પાસવર્ડ સેટ કરો"), + ("Enable keyboard/mouse", "કીબોર્ડ/માઉસ સક્ષમ કરો"), + ("Enable clipboard", "ક્લિપબોર્ડ સક્ષમ કરો"), + ("Enable file transfer", "ફાઇલ ટ્રાન્સફર સક્ષમ કરો"), + ("Enable TCP tunneling", "TCP ટનલિંગ સક્ષમ કરો"), + ("IP Whitelisting", "IP વ્હાઇટલિસ્ટિંગ"), + ("ID/Relay Server", "ID/રિલે સર્વર"), + ("Import server config", "સર્વર કોન્ફિગ ઈમ્પોર્ટ કરો"), + ("Export Server Config", "સર્વર કોન્ફિગ એક્સપોર્ટ કરો"), + ("Import server configuration successfully", "સર્વર કોન્ફિગરેશન સફળતાપૂર્વક ઈમ્પોર્ટ થયું"), + ("Export server configuration successfully", "સર્વર કોન્ફિગરેશન સફળતાપૂર્વક એક્સપોર્ટ થયું"), + ("Invalid server configuration", "અમાન્ય સર્વર કોન્ફિગરેશન"), + ("Clipboard is empty", "ક્લિપબોર્ડ ખાલી છે"), + ("Stop service", "સેવા બંધ કરો"), + ("Change ID", "ID બદલો"), + ("Your new ID", "તમારું નવું ID"), + ("length %min% to %max%", "લંબાઈ %min% થી %max% સુધી"), + ("starts with a letter", "અક્ષરથી શરૂ થાય છે"), + ("allowed characters", "માન્ય અક્ષરો"), + ("id_change_tip", "ID બદલ્યા પછી વર્તમાન કનેક્શન તૂટી જશે."), + ("Website", "વેબસાઇટ"), + ("About", "વિશે"), + ("Slogan_tip", "વધુ સારા અનુભવ માટે બનાવેલ રિમોટ ડેસ્કટોપ સોફ્ટવેર"), + ("Privacy Statement", "ગોપનીયતા નિવેદન"), + ("Mute", "મ્યૂટ કરો"), + ("Build Date", "બિલ્ડ તારીખ"), + ("Version", "સંસ્કરણ (Version)"), + ("Home", "હોમ"), + ("Audio Input", "ઓડિયો ઇનપુટ"), + ("Enhancements", "વધારાની સુવિધાઓ"), + ("Hardware Codec", "હાર્ડવેર કોડેક"), + ("Adaptive bitrate", "એડેપ્ટિવ બિટરેટ"), + ("ID Server", "ID સર્વર"), + ("Relay Server", "રિલે સર્વર"), + ("API Server", "API સર્વર"), + ("invalid_http", "અમાન્ય HTTP લિંક"), + ("Invalid IP", "અમાન્ય IP"), + ("Invalid format", "અમાન્ય ફોર્મેટ"), + ("server_not_support", "સર્વર દ્વારા સમર્થિત નથી"), + ("Not available", "ઉપલબ્ધ નથી"), + ("Too frequent", "ખૂબ વારંવાર"), + ("Cancel", "રદ કરો"), + ("Skip", "રહેવા દો (Skip)"), + ("Close", "બંધ કરો"), + ("Retry", "ફરી પ્રયાસ કરો"), + ("OK", "બરાબર"), + ("Password Required", "પાસવર્ડ જરૂરી છે"), + ("Please enter your password", "કૃપા કરીને તમારો પાસવર્ડ દાખલ કરો"), + ("Remember password", "પાસવર્ડ યાદ રાખો"), + ("Wrong Password", "ખોટો પાસવર્ડ"), + ("Do you want to enter again?", "શું તમે ફરીથી દાખલ કરવા માંગો છો?"), + ("Connection Error", "કનેક્શન ભૂલ"), + ("Error", "ભૂલ"), + ("Reset by the peer", "સામેના છેડેથી રિસેટ કરવામાં આવ્યું"), + ("Connecting...", "જોડાઈ રહ્યું છે..."), + ("Connection in progress. Please wait.", "કનેક્શન ચાલુ છે. કૃપા કરીને રાહ જુઓ."), + ("Please try 1 minute later", "કૃપા કરીને 1 મિનિટ પછી ફરી પ્રયાસ કરો"), + ("Login Error", "લોગિન ભૂલ"), + ("Successful", "સફળ"), + ("Connected, waiting for image...", "જોડાયેલ, ઇમેજની રાહ જોવાય છે..."), + ("Name", "નામ"), + ("Type", "પ્રકાર"), + ("Modified", "સુધારેલ"), + ("Size", "કદ (Size)"), + ("Show Hidden Files", "છુપાયેલી ફાઇલો બતાવો"), + ("Receive", "મેળવો"), + ("Send", "મોકલો"), + ("Refresh File", "ફાઇલ રિફ્રેશ કરો"), + ("Local", "લોકલ"), + ("Remote", "રિમોટ"), + ("Remote Computer", "રિમોટ કોમ્પ્યુટર"), + ("Local Computer", "લોકલ કોમ્પ્યુટર"), + ("Confirm Delete", "કાઢી નાખવાની પુષ્ટિ કરો"), + ("Delete", "કાઢી નાખો"), + ("Properties", "ગુણધર્મો (Properties)"), + ("Multi Select", "બહુ-પસંદગી"), + ("Select All", "બધું પસંદ કરો"), + ("Unselect All", "બધું નાપસંદ કરો"), + ("Empty Directory", "ખાલી ડિરેક્ટરી"), + ("Not an empty directory", "ડિરેક્ટરી ખાલી નથી"), + ("Are you sure you want to delete this file?", "શું તમે ખરેખર આ ફાઇલ કાઢી નાખવા માંગો છો?"), + ("Are you sure you want to delete this empty directory?", "શું તમે ખરેખર આ ખાલી ડિરેક્ટરી કાઢી નાખવા માંગો છો?"), + ("Are you sure you want to delete the file of this directory?", "શું તમે ખરેખર આ ડિરેક્ટરીની ફાઇલ કાઢી નાખવા માંગો છો?"), + ("Do this for all conflicts", "તમામ વિવાદો માટે આ કરો"), + ("This is irreversible!", "આ બદલી શકાશે નહીં!"), + ("Deleting", "કાઢી નાખવામાં આવી રહ્યું છે"), + ("files", "ફાઇલો"), + ("Waiting", "રાહ જુઓ"), + ("Finished", "પૂરું થયું"), + ("Speed", "ગતિ"), + ("Custom Image Quality", "કસ્ટમ ઇમેજ ગુણવત્તા"), + ("Privacy mode", "પ્રાઇવસી મોડ"), + ("Block user input", "યુઝર ઇનપુટ બ્લોક કરો"), + ("Unblock user input", "યુઝર ઇનપુટ અનબ્લોક કરો"), + ("Adjust Window", "વિન્ડો એડજસ્ટ કરો"), + ("Original", "મૂળ (Original)"), + ("Shrink", "સંકોચો (Shrink)"), + ("Stretch", "ખેંચો (Stretch)"), + ("Scrollbar", "સ્ક્રોલબાર"), + ("ScrollAuto", "ઓટો સ્ક્રોલ"), + ("Good image quality", "સારી ઇમેજ ગુણવત્તા"), + ("Balanced", "સંતુલિત"), + ("Optimize reaction time", "પ્રતિક્રિયા સમય શ્રેષ્ઠ બનાવો"), + ("Custom", "કસ્ટમ"), + ("Show remote cursor", "રિમોટ કર્સર બતાવો"), + ("Show quality monitor", "ક્વોલિટી મોનિટર બતાવો"), + ("Disable clipboard", "ક્લિપબોર્ડ અક્ષમ કરો"), + ("Lock after session end", "સત્ર સમાપ્ત થયા પછી લોક કરો"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del દાખલ કરો"), + ("Insert Lock", "લોક દાખલ કરો"), + ("Refresh", "રિફ્રેશ કરો"), + ("ID does not exist", "ID અસ્તિત્વમાં નથી"), + ("Failed to connect to rendezvous server", "Rendezvous સર્વર સાથે જોડવામાં નિષ્ફળ"), + ("Please try later", "કૃપા કરીને પછી પ્રયાસ કરો"), + ("Remote desktop is offline", "રિમોટ ડેસ્કટોપ ઓફલાઇન છે"), + ("Key mismatch", "કી મેળ ખાતી નથી"), + ("Timeout", "સમય સમાપ્ત"), + ("Failed to connect to relay server", "રિલે સર્વર સાથે જોડવામાં નિષ્ફળ"), + ("Failed to connect via rendezvous server", "Rendezvous સર્વર દ્વારા જોડવામાં નિષ્ફળ"), + ("Failed to connect via relay server", "રિલે સર્વર દ્વારા જોડવામાં નિષ્ફળ"), + ("Failed to make direct connection to remote desktop", "રિમોટ ડેસ્કટોપ સાથે સીધું જોડાણ કરવામાં નિષ્ફળ"), + ("Set Password", "પાસવર્ડ સેટ કરો"), + ("OS Password", "OS પાસવર્ડ"), + ("install_tip", "શ્રેષ્ઠ પ્રદર્શન માટે, કૃપા કરીને ઇન્સ્ટોલ કરો."), + ("Click to upgrade", "અપગ્રેડ કરવા માટે ક્લિક કરો"), + ("Configure", "કોન્ફિગર કરો"), + ("config_acc", "એક્સેસિબિલિટી કોન્ફિગર કરો"), + ("config_screen", "સ્ક્રીન કોન્ફિગર કરો"), + ("Installing ...", "ઇન્સ્ટોલ થઈ રહ્યું છે..."), + ("Install", "ઇન્સ્ટોલ કરો"), + ("Installation", "ઇન્સ્ટોલેશન"), + ("Installation Path", "ઇન્સ્ટોલેશન પાથ"), + ("Create start menu shortcuts", "સ્ટાર્ટ મેનૂ શોર્ટકટ બનાવો"), + ("Create desktop icon", "ડેસ્કટોપ આઇકોન બનાવો"), + ("agreement_tip", "ઇન્સ્ટોલ કરીને તમે લાયસન્સ કરાર સ્વીકારો છો."), + ("Accept and Install", "સ્વીકારો અને ઇન્સ્ટોલ કરો"), + ("End-user license agreement", "અંતિમ વપરાશકર્તા લાયસન્સ કરાર"), + ("Generating ...", "જનરેટ થઈ રહ્યું છે..."), + ("Your installation is lower version.", "તમારું ઇન્સ્ટોલેશન જૂનું સંસ્કરણ છે."), + ("not_close_tcp_tip", "ટનલનો ઉપયોગ કરતી વખતે આ વિન્ડો બંધ કરશો નહીં."), + ("Listening ...", "સાંભળી રહ્યું છે..."), + ("Remote Host", "રિમોટ હોસ્ટ"), + ("Remote Port", "રિમોટ પોર્ટ"), + ("Action", "ક્રિયા"), + ("Add", "ઉમેરો"), + ("Local Port", "લોકલ પોર્ટ"), + ("Local Address", "લોકલ સરનામું"), + ("Change Local Port", "લોકલ પોર્ટ બદલો"), + ("setup_server_tip", "ઝડપી કનેક્શન માટે તમારું પોતાનું સર્વર સેટ કરો"), + ("Too short, at least 6 characters.", "ખૂબ ટૂંકું, ઓછામાં ઓછા 6 અક્ષરો હોવા જોઈએ."), + ("The confirmation is not identical.", "પુષ્ટિકરણ સરખું નથી."), + ("Permissions", "પરવાનગીઓ"), + ("Accept", "સ્વીકારો"), + ("Dismiss", "ખારીજ કરો"), + ("Disconnect", "ડિસ્કનેક્ટ કરો"), + ("Enable file copy and paste", "ફાઇલ કોપી અને પેસ્ટ સક્ષમ કરો"), + ("Connected", "જોડાયેલ"), + ("Direct and encrypted connection", "સીધું અને એન્ક્રિપ્ટેડ કનેક્શન"), + ("Relayed and encrypted connection", "રિલે અને એન્ક્રિપ્ટેડ કનેક્શન"), + ("Direct and unencrypted connection", "સીધું અને અનએન્ક્રિપ્ટેડ કનેક્શન"), + ("Relayed and unencrypted connection", "રિલે અને અનએન્ક્રિપ્ટેડ કનેક્શન"), + ("Enter Remote ID", "રિમોટ ID દાખલ કરો"), + ("Enter your password", "તમારો પાસવર્ડ દાખલ કરો"), + ("Logging in...", "લોગિન થઈ રહ્યું છે..."), + ("Enable RDP session sharing", "RDP સત્ર શેરિંગ સક્ષમ કરો"), + ("Auto Login", "ઓટો લોગિન"), + ("Enable direct IP access", "સીધું IP એક્સેસ સક્ષમ કરો"), + ("Rename", "નામ બદલો"), + ("Space", "જગ્યા (Space)"), + ("Create desktop shortcut", "ડેસ્કટોપ શોર્ટકટ બનાવો"), + ("Change Path", "પાથ બદલો"), + ("Create Folder", "ફોલ્ડર બનાવો"), + ("Please enter the folder name", "કૃપા કરીને ફોલ્ડરનું નામ દાખલ કરો"), + ("Fix it", "તેને ઠીક કરો"), + ("Warning", "ચેતવણી"), + ("Login screen using Wayland is not supported", "Wayland ઉપયોગ કરતી લોગિન સ્ક્રીન સમર્થિત નથી"), + ("Reboot required", "રિબૂટ જરૂરી છે"), + ("Unsupported display server", "અસમર્થિત ડિસ્પ્લે સર્વર"), + ("x11 expected", "x11 અપેક્ષિત છે"), + ("Port", "પોર્ટ"), + ("Settings", "સેટિંગ્સ"), + ("Username", "વપરાશકર્તા નામ"), + ("Invalid port", "અમાન્ય પોર્ટ"), + ("Closed manually by the peer", "સામેથી મેન્યુઅલી બંધ કરવામાં આવ્યું"), + ("Enable remote configuration modification", "રિમોટ કોન્ફિગરેશન ફેરફાર સક્ષમ કરો"), + ("Run without install", "ઇન્સ્ટોલ કર્યા વગર ચલાવો"), + ("Connect via relay", "રિલે દ્વારા કનેક્ટ કરો"), + ("Always connect via relay", "હંમેશા રિલે દ્વારા કનેક્ટ કરો"), + ("whitelist_tip", "માત્ર વ્હાઇટલિસ્ટ કરેલ IP જ મને એક્સેસ કરી શકે છે"), + ("Login", "લોગિન"), + ("Verify", "ચકાસો"), + ("Remember me", "મને યાદ રાખો"), + ("Trust this device", "આ ઉપકરણ પર વિશ્વાસ કરો"), + ("Verification code", "વેરિફિકેશન કોડ"), + ("verification_tip", "વેરિફિકેશન કોડ તમારા ઇમેઇલ પર મોકલવામાં આવ્યો છે"), + ("Logout", "લોગઆઉટ"), + ("Tags", "ટેગ્સ"), + ("Search ID", "ID શોધો"), + ("whitelist_sep", "અલ્પવિરામ, અર્ધવિરામ અથવા સ્પેસ દ્વારા અલગ કરો"), + ("Add ID", "ID ઉમેરો"), + ("Add Tag", "ટેગ ઉમેરો"), + ("Unselect all tags", "તમામ ટેગ નાપસંદ કરો"), + ("Network error", "નેટવર્ક ભૂલ"), + ("Username missed", "વપરાશકર્તા નામ બાકી છે"), + ("Password missed", "પાસવર્ડ બાકી છે"), + ("Wrong credentials", "ખોટી વિગતો"), + ("The verification code is incorrect or has expired", "વેરિફિકેશન કોડ ખોટો છે અથવા તેની મર્યાદા પૂરી થઈ ગઈ છે"), + ("Edit Tag", "ટેગ સુધારો"), + ("Forget Password", "પાસવર્ડ ભૂલી ગયા"), + ("Favorites", "પસંદગીના"), + ("Add to Favorites", "પસંદગીમાં ઉમેરો"), + ("Remove from Favorites", "પસંદગીમાંથી દૂર કરો"), + ("Empty", "ખાલી"), + ("Invalid folder name", "અમાન્ય ફોલ્ડર નામ"), + ("Socks5 Proxy", "Socks5 પ્રોક્સી"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) પ્રોક્સી"), + ("Discovered", "શોધાયેલ"), + ("install_daemon_tip", "બૂટ વખતે શરૂ કરવા માટે સેવા ઇન્સ્ટોલ કરો"), + ("Remote ID", "રિમોટ ID"), + ("Paste", "પેસ્ટ કરો"), + ("Paste here?", "અહીં પેસ્ટ કરવું છે?"), + ("Are you sure to close the connection?", "શું તમે ખરેખર કનેક્શન બંધ કરવા માંગો છો?"), + ("Download new version", "નવું સંસ્કરણ ડાઉનલોડ કરો"), + ("Touch mode", "ટચ મોડ"), + ("Mouse mode", "માઉસ મોડ"), + ("One-Finger Tap", "એક આંગળીથી ટેપ"), + ("Left Mouse", "ડાબું માઉસ બટન"), + ("One-Long Tap", "એક લાંબો ટેપ"), + ("Two-Finger Tap", "બે આંગળીથી ટેપ"), + ("Right Mouse", "જમણું માઉસ બટન"), + ("One-Finger Move", "એક આંગળીથી હલનચલન"), + ("Double Tap & Move", "ડબલ ટેપ અને હલનચલન"), + ("Mouse Drag", "માઉસ ડ્રેગ"), + ("Three-Finger vertically", "ત્રણ આંગળી ઊભી રીતે"), + ("Mouse Wheel", "માઉસ વ્હીલ"), + ("Two-Finger Move", "બે આંગળીથી હલનચલન"), + ("Canvas Move", "કેનવાસ ખસેડો"), + ("Pinch to Zoom", "ઝૂમ કરવા માટે પિંચ કરો"), + ("Canvas Zoom", "કેનવાસ ઝૂમ"), + ("Reset canvas", "કેનવાસ રિસેટ કરો"), + ("No permission of file transfer", "ફાઇલ ટ્રાન્સફરની પરવાનગી નથી"), + ("Note", "નોંધ"), + ("Connection", "કનેક્શન"), + ("Share screen", "સ્ક્રીન શેર કરો"), + ("Chat", "ચેટ"), + ("Total", "કુલ"), + ("items", "વસ્તુઓ"), + ("Selected", "પસંદ કરેલ"), + ("Screen Capture", "સ્ક્રીન કેપ્ચર"), + ("Input Control", "ઇનપુટ નિયંત્રણ"), + ("Audio Capture", "ઓડિયો કેપ્ચર"), + ("Do you accept?", "શું તમે સ્વીકારો છો?"), + ("Open System Setting", "સિસ્ટમ સેટિંગ ખોલો"), + ("How to get Android input permission?", "Android ઇનપુટ પરવાનગી કેવી રીતે મેળવવી?"), + ("android_input_permission_tip1", "ઇનપુટ પરવાનગી મેળવવા માટે એક્સેસિબિલિટી સેવા સક્ષમ કરો."), + ("android_input_permission_tip2", "કૃપા કરીને સેટિંગ્સમાં RustDesk શોધો અને તેને ચાલુ કરો."), + ("android_new_connection_tip", "નવો કંટ્રોલ વિનંતી પ્રાપ્ત થઈ છે."), + ("android_service_will_start_tip", "સ્ક્રીન કેપ્ચર ચાલુ કરવાથી સેવા આપમેળે શરૂ થશે."), + ("android_stop_service_tip", "સેવા બંધ કરવાથી તમામ કનેક્શન બંધ થઈ જશે."), + ("android_version_audio_tip", "ઓડિયો કેપ્ચર માત્ર Android 10 કે તેથી ઉપરના વર્ઝનમાં ઉપલબ્ધ છે."), + ("android_start_service_tip", "સ્ક્રીન શેરિંગ સેવા શરૂ કરવા ક્લિક કરો."), + ("android_permission_may_not_change_tip", "પરવાનગીઓ પછીથી બદલી શકાશે નહીં, કૃપા કરીને કાળજીપૂર્વક પસંદ કરો."), + ("Account", "ખાતું"), + ("Overwrite", "ઓવરરાઇટ કરો"), + ("This file exists, skip or overwrite this file?", "આ ફાઇલ અસ્તિત્વમાં છે, રહેવા દેવી છે કે ઓવરરાઇટ કરવી છે?"), + ("Quit", "બહાર નીકળો"), + ("Help", "મદદ"), + ("Failed", "નિષ્ફળ"), + ("Succeeded", "સફળ"), + ("Someone turns on privacy mode, exit", "કોઈએ પ્રાઇવસી મોડ ચાલુ કર્યો છે, બહાર નીકળો"), + ("Unsupported", "અસમર્થિત"), + ("Peer denied", "સામેથી નકારવામાં આવ્યું"), + ("Please install plugins", "કૃપા કરીને પ્લગઇન્સ ઇન્સ્ટોલ કરો"), + ("Peer exit", "સામેથી કોઈ બહાર નીકળી ગયું"), + ("Failed to turn off", "બંધ કરવામાં નિષ્ફળ"), + ("Turned off", "બંધ કરવામાં આવ્યું"), + ("Language", "ભાષા"), + ("Keep RustDesk background service", "RustDesk બેકગ્રાઉન્ડ સેવા ચાલુ રાખો"), + ("Ignore Battery Optimizations", "બેટરી ઓપ્ટિમાઇઝેશન અવગણો"), + ("android_open_battery_optimizations_tip", "ડિસ્કનેક્શન ટાળવા માટે બેટરી ઓપ્ટિમાઇઝેશન સેટિંગ ખોલો"), + ("Start on boot", "બૂટ પર શરૂ કરો"), + ("Start the screen sharing service on boot, requires special permissions", "બૂટ પર સ્ક્રીન શેરિંગ શરૂ કરો, ખાસ પરવાનગીની જરૂર છે"), + ("Connection not allowed", "કનેક્શનની પરવાનગી નથી"), + ("Legacy mode", "લેગસી મોડ"), + ("Map mode", "મેપ મોડ"), + ("Translate mode", "અનુવાદ મોડ"), + ("Use permanent password", "કાયમી પાસવર્ડનો ઉપયોગ કરો"), + ("Use both passwords", "બંને પાસવર્ડનો ઉપયોગ કરો"), + ("Set permanent password", "કાયમી પાસવર્ડ સેટ કરો"), + ("Enable remote restart", "રિમોટ રિસ્ટાર્ટ સક્ષમ કરો"), + ("Restart remote device", "રિમોટ ઉપકરણ રિસ્ટાર્ટ કરો"), + ("Are you sure you want to restart", "શું તમે ખરેખર રિસ્ટાર્ટ કરવા માંગો છો?"), + ("Restarting remote device", "રિમોટ ઉપકરણ રિસ્ટાર્ટ થઈ રહ્યું છે"), + ("remote_restarting_tip", "રિમોટ ઉપકરણ રિસ્ટાર્ટ થઈ રહ્યું છે, કૃપા કરીને રાહ જુઓ..."), + ("Copied", "કોપી થઈ ગયું"), + ("Exit Fullscreen", "ફુલસ્ક્રીનમાંથી બહાર નીકળો"), + ("Fullscreen", "ફુલસ્ક્રીન"), + ("Mobile Actions", "મોબાઇલ ક્રિયાઓ"), + ("Select Monitor", "મોનિટર પસંદ કરો"), + ("Control Actions", "નિયંત્રણ ક્રિયાઓ"), + ("Display Settings", "ડિસ્પ્લે સેટિંગ્સ"), + ("Ratio", "રેશિયો (Ratio)"), + ("Image Quality", "ઇમેજ ગુણવત્તા"), + ("Scroll Style", "સ્ક્રોલ શૈલી"), + ("Show Toolbar", "ટૂલબાર બતાવો"), + ("Hide Toolbar", "ટૂલબાર છુપાવો"), + ("Direct Connection", "સીધું કનેક્શન"), + ("Relay Connection", "રિલે કનેક્શન"), + ("Secure Connection", "સુરક્ષિત કનેક્શન"), + ("Insecure Connection", "અસુરક્ષિત કનેક્શન"), + ("Scale original", "મૂળ સ્કેલ"), + ("Scale adaptive", "એડેપ્ટિવ સ્કેલ"), + ("General", "સામાન્ય"), + ("Security", "સુરક્ષા"), + ("Theme", "થીમ"), + ("Dark Theme", "ડાર્ક થીમ"), + ("Light Theme", "લાઇટ થીમ"), + ("Dark", "ડાર્ક"), + ("Light", "લાઇટ"), + ("Follow System", "સિસ્ટમ મુજબ"), + ("Enable hardware codec", "હાર્ડવેર કોડેક સક્ષમ કરો"), + ("Unlock Security Settings", "સુરક્ષા સેટિંગ્સ અનલોક કરો"), + ("Enable audio", "ઓડિયો સક્ષમ કરો"), + ("Unlock Network Settings", "નેટવર્ક સેટિંગ્સ અનલોક કરો"), + ("Server", "સર્વર"), + ("Direct IP Access", "સીધું IP એક્સેસ"), + ("Proxy", "પ્રોક્સી"), + ("Apply", "લાગુ કરો"), + ("Disconnect all devices?", "તમામ ઉપકરણો ડિસ્કનેક્ટ કરવા છે?"), + ("Clear", "સાફ કરો"), + ("Audio Input Device", "ઓડિયો ઇનપુટ ઉપકરણ"), + ("Use IP Whitelisting", "IP વ્હાઇટલિસ્ટિંગનો ઉપયોગ કરો"), + ("Network", "નેટવર્ક"), + ("Pin Toolbar", "ટૂલબાર પિન કરો"), + ("Unpin Toolbar", "ટૂલબાર અનપિન કરો"), + ("Recording", "રેકોર્ડિંગ"), + ("Directory", "ડિરેક્ટરી"), + ("Automatically record incoming sessions", "આવતા સત્રો આપમેળે રેકોર્ડ કરો"), + ("Automatically record outgoing sessions", "જતા સત્રો આપમેળે રેકોર્ડ કરો"), + ("Change", "બદલો"), + ("Start session recording", "સત્ર રેકોર્ડિંગ શરૂ કરો"), + ("Stop session recording", "સત્ર રેકોર્ડિંગ બંધ કરો"), + ("Enable recording session", "સત્ર રેકોર્ડિંગ સક્ષમ કરો"), + ("Enable LAN discovery", "LAN ડિસ્કવરી સક્ષમ કરો"), + ("Deny LAN discovery", "LAN ડિસ્કવરી નકારો"), + ("Write a message", "સંદેશ લખો"), + ("Prompt", "પ્રોમ્પ્ટ"), + ("Please wait for confirmation of UAC...", "કૃપા કરીને UAC પુષ્ટિની રાહ જુઓ..."), + ("elevated_foreground_window_tip", "રિમોટની વર્તમાન વિન્ડોને વધારે પરવાનગીની જરૂર છે."), + ("Disconnected", "ડિસ્કનેક્ટ થઈ ગયું"), + ("Other", "અન્ય"), + ("Confirm before closing multiple tabs", "બહુવિધ ટેબ્સ બંધ કરતા પહેલા પુષ્ટિ કરો"), + ("Keyboard Settings", "કીબોર્ડ સેટિંગ્સ"), + ("Full Access", "પૂર્ણ એક્સેસ"), + ("Screen Share", "સ્ક્રીન શેર"), + ("ubuntu-21-04-required", "Ubuntu 21.04 કે તેથી ઉપર જરૂરી છે"), + ("wayland-requires-higher-linux-version", "Wayland માટે ઉચ્ચ Linux વર્ઝન જરૂરી છે"), + ("xdp-portal-unavailable", "XDP પોર્ટલ અનુપલબ્ધ છે"), + ("JumpLink", "JumpLink"), + ("Please Select the screen to be shared(Operate on the peer side).", "કૃપા કરીને શેર કરવાની સ્ક્રીન પસંદ કરો (સામેના છેડે કાર્ય કરો)."), + ("Show RustDesk", "RustDesk બતાવો"), + ("This PC", "આ PC"), + ("or", "અથવા"), + ("Elevate", "એલિવેટ કરો"), + ("Zoom cursor", "ઝૂમ કર્સર"), + ("Accept sessions via password", "પાસવર્ડ દ્વારા સત્રો સ્વીકારો"), + ("Accept sessions via click", "ક્લિક દ્વારા સત્રો સ્વીકારો"), + ("Accept sessions via both", "બંને દ્વારા સત્રો સ્વીકારો"), + ("Please wait for the remote side to accept your session request...", "કૃપા કરીને સામેનો છેડો વિનંતી સ્વીકારે તેની રાહ જુઓ..."), + ("One-time Password", "વન-ટાઇમ પાસવર્ડ (OTP)"), + ("Use one-time password", "વન-ટાઇમ પાસવર્ડનો ઉપયોગ કરો"), + ("One-time password length", "OTP ની લંબાઈ"), + ("Request access to your device", "તમારા ઉપકરણના એક્સેસ માટે વિનંતી"), + ("Hide connection management window", "કનેક્શન મેનેજમેન્ટ વિન્ડો છુપાવો"), + ("hide_cm_tip", "જો પાસવર્ડ દ્વારા કનેક્શન હોય તો જ છુપાવો"), + ("wayland_experiment_tip", "Wayland સપોર્ટ હજુ પ્રાયોગિક ધોરણે છે"), + ("Right click to select tabs", "ટેબ્સ પસંદ કરવા રાઇટ ક્લિક કરો"), + ("Skipped", "રહેવા દીધું (Skipped)"), + ("Add to address book", "એડ્રેસ બુકમાં ઉમેરો"), + ("Group", "ગ્રુપ"), + ("Search", "શોધો"), + ("Closed manually by web console", "વેબ કન્સોલ દ્વારા મેન્યુઅલી બંધ કરવામાં આવ્યું"), + ("Local keyboard type", "લોકલ કીબોર્ડ પ્રકાર"), + ("Select local keyboard type", "લોકલ કીબોર્ડ પ્રકાર પસંદ કરો"), + ("software_render_tip", "જો સ્ક્રીન કાળી દેખાય, તો આ અજમાવો"), + ("Always use software rendering", "હંમેશા સોફ્ટવેર રેન્ડરિંગનો ઉપયોગ કરો"), + ("config_input", "ઇનપુટ કોન્ફિગર કરો"), + ("config_microphone", "માઇક્રોફોન કોન્ફિગર કરો"), + ("request_elevation_tip", "સામેથી ઉચ્ચ પરવાનગી (Elevation) માટે વિનંતી કરો"), + ("Wait", "રાહ જુઓ"), + ("Elevation Error", "એલિવેશન ભૂલ"), + ("Ask the remote user for authentication", "સામેના યુઝરને ઓથેન્ટિકેશન માટે પૂછો"), + ("Choose this if the remote account is administrator", "જો સામેનું ખાતું એડમિનિસ્ટ્રેટર હોય તો આ પસંદ કરો"), + ("Transmit the username and password of administrator", "એડમિનિસ્ટ્રેટરનું નામ અને પાસવર્ડ મોકલો"), + ("still_click_uac_tip", "રિમોટ યુઝરે હજુ પણ UAC વિન્ડોમાં 'હા' ક્લિક કરવું પડશે."), + ("Request Elevation", "એલિવેશન માટે વિનંતી કરો"), + ("wait_accept_uac_tip", "કૃપા કરીને સામેનો યુઝર UAC સ્વીકારે તેની રાહ જુઓ."), + ("Elevate successfully", "સફળતાપૂર્વક એલિવેટ થયું"), + ("uppercase", "મોટા અક્ષરો (Uppercase)"), + ("lowercase", "નાના અક્ષરો (Lowercase)"), + ("digit", "અંક (Digit)"), + ("special character", "ખાસ અક્ષર"), + ("length>=8", "લંબાઈ >= 8"), + ("Weak", "નબળું"), + ("Medium", "મધ્યમ"), + ("Strong", "મજબૂત"), + ("Switch Sides", "બાજુઓ બદલો"), + ("Please confirm if you want to share your desktop?", "શું તમે તમારું ડેસ્કટોપ શેર કરવા માંગો છો?"), + ("Display", "ડિસ્પ્લે"), + ("Default View Style", "ડિફોલ્ટ વ્યુ શૈલી"), + ("Default Scroll Style", "ડિફોલ્ટ સ્ક્રોલ શૈલી"), + ("Default Image Quality", "ડિફોલ્ટ ઇમેજ ગુણવત્તા"), + ("Default Codec", "ડિફોલ્ટ કોડેક"), + ("Bitrate", "બિટરેટ"), + ("FPS", "FPS"), + ("Auto", "ઓટો"), + ("Other Default Options", "અન્ય ડિફોલ્ટ વિકલ્પો"), + ("Voice call", "વોઇસ કોલ"), + ("Text chat", "ટેક્સ્ટ ચેટ"), + ("Stop voice call", "વોઇસ કોલ બંધ કરો"), + ("relay_hint_tip", "સીધું કનેક્શન શક્ય નથી; તમે રિલે દ્વારા પ્રયાસ કરી શકો છો."), + ("Reconnect", "ફરી કનેક્ટ કરો"), + ("Codec", "કોડેક"), + ("Resolution", "રિઝોલ્યુશન"), + ("No transfers in progress", "કોઈ ટ્રાન્સફર ચાલુ નથી"), + ("Set one-time password length", "OTP લંબાઈ સેટ કરો"), + ("RDP Settings", "RDP સેટિંગ્સ"), + ("Sort by", "ક્રમબદ્ધ કરો"), + ("New Connection", "નવું કનેક્શન"), + ("Restore", "રીસ્ટોર"), + ("Minimize", "મિનિમાઇઝ"), + ("Maximize", "મેક્સિમાઇઝ"), + ("Your Device", "તમારું ઉપકરણ"), + ("empty_recent_tip", "તાજેતરના સત્રો અહીં દેખાશે."), + ("empty_favorite_tip", "પસંદગીના ઉપકરણો અહીં દેખાશે."), + ("empty_lan_tip", "નેટવર્ક પરના ઉપકરણો અહીં દેખાશે."), + ("empty_address_book_tip", "તમારી એડ્રેસ બુક ખાલી છે."), + ("Empty Username", "ખાલી યુઝરનેમ"), + ("Empty Password", "ખાલી પાસવર્ડ"), + ("Me", "હું"), + ("identical_file_tip", "આ ફાઇલ પહેલેથી જ અસ્તિત્વમાં છે."), + ("show_monitors_tip", "ટૂલબારમાં મોનિટર બતાવો"), + ("View Mode", "વ્યુ મોડ"), + ("login_linux_tip", "રિમોટ Linux સત્ર માટે તમારે લોગિન કરવું પડશે"), + ("verify_rustdesk_password_tip", "RustDesk પાસવર્ડ ચકાસો"), + ("remember_account_tip", "આ ખાતું યાદ રાખો"), + ("os_account_desk_tip", "એક્સેસ માટે OS ખાતાનો ઉપયોગ કરો"), + ("OS Account", "OS ખાતું"), + ("another_user_login_title_tip", "બીજો યુઝર પહેલેથી લોગિન છે"), + ("another_user_login_text_tip", "ડિસ્કનેક્ટ કરો અને ફરી પ્રયાસ કરો"), + ("xorg_not_found_title_tip", "Xorg મળ્યું નથી"), + ("xorg_not_found_text_tip", "કૃપા કરીને Xorg ઇન્સ્ટોલ કરો"), + ("no_desktop_title_tip", "કોઈ ડેસ્કટોપ ઉપલબ્ધ નથી"), + ("no_desktop_text_tip", "કૃપા કરીને Linux ડેસ્કટોપ ઇન્સ્ટોલ કરો"), + ("No need to elevate", "એલિવેટ કરવાની જરૂર નથી"), + ("System Sound", "સિસ્ટમ સાઉન્ડ"), + ("Default", "ડિફોલ્ટ"), + ("New RDP", "નવું RDP"), + ("Fingerprint", "ફિંગરપ્રિન્ટ"), + ("Copy Fingerprint", "ફિંગરપ્રિન્ટ કોપી કરો"), + ("no fingerprints", "કોઈ ફિંગરપ્રિન્ટ નથી"), + ("Select a peer", "એક પીઅર પસંદ કરો"), + ("Select peers", "પીઅર્સ પસંદ કરો"), + ("Plugins", "પ્લગઇન્સ"), + ("Uninstall", "અનઇન્સ્ટોલ કરો"), + ("Update", "અપડેટ કરો"), + ("Enable", "સક્ષમ કરો"), + ("Disable", "અક્ષમ કરો"), + ("Options", "વિકલ્પો"), + ("resolution_original_tip", "મૂળ રિઝોલ્યુશન"), + ("resolution_fit_local_tip", "સ્ક્રીન મુજબ ફીટ કરો"), + ("resolution_custom_tip", "કસ્ટમ રિઝોલ્યુશન"), + ("Collapse toolbar", "ટૂલબાર નાનું કરો"), + ("Accept and Elevate", "સ્વીકારો અને એલિવેટ કરો"), + ("accept_and_elevate_btn_tooltip", "કનેક્શન સ્વીકારો અને UAC પરવાનગીઓ મેળવો."), + ("clipboard_wait_response_timeout_tip", "ક્લિપબોર્ડ પ્રતિક્રિયા માટે સમય સમાપ્ત થયો."), + ("Incoming connection", "આવતું કનેક્શન"), + ("Outgoing connection", "જતું કનેક્શન"), + ("Exit", "બહાર નીકળો"), + ("Open", "ખોલો"), + ("logout_tip", "શું તમે ખરેખર લોગઆઉટ કરવા માંગો છો?"), + ("Service", "સેવા"), + ("Start", "શરૂ કરો"), + ("Stop", "બંધ કરો"), + ("exceed_max_devices", "તમે ઉપકરણોની મહત્તમ મર્યાદા વટાવી દીધી છે."), + ("Sync with recent sessions", "તાજેતરના સત્રો સાથે સિંક કરો"), + ("Sort tags", "ટેગ્સ ક્રમબદ્ધ કરો"), + ("Open connection in new tab", "નવી ટેબમાં કનેક્શન ખોલો"), + ("Move tab to new window", "ટેબને નવી વિન્ડોમાં ખસેડો"), + ("Can not be empty", "ખાલી ન હોઈ શકે"), + ("Already exists", "પહેલેથી અસ્તિત્વમાં છે"), + ("Change Password", "પાસવર્ડ બદલો"), + ("Refresh Password", "પાસવર્ડ રિફ્રેશ કરો"), + ("ID", "ID"), + ("Grid View", "ગ્રીડ વ્યુ"), + ("List View", "લિસ્ટ વ્યુ"), + ("Select", "પસંદ કરો"), + ("Toggle Tags", "ટેગ્સ ચાલુ/બંધ કરો"), + ("pull_ab_failed_tip", "એડ્રેસ બુક અપડેટ કરવામાં નિષ્ફળ."), + ("push_ab_failed_tip", "એડ્રેસ બુક સિંક કરવામાં નિષ્ફળ."), + ("synced_peer_readded_tip", "તાજેતરના સત્રોના ઉપકરણો એડ્રેસ બુકમાં સિંક થયા."), + ("Change Color", "રંગ બદલો"), + ("Primary Color", "પ્રાથમિક રંગ"), + ("HSV Color", "HSV રંગ"), + ("Installation Successful!", "ઇન્સ્ટોલેશન સફળ!"), + ("Installation failed!", "ઇન્સ્ટોલેશન નિષ્ફળ!"), + ("Reverse mouse wheel", "માઉસ વ્હીલ ઊલટું કરો"), + ("{} sessions", "{} સત્રો"), + ("scam_title", "છેતરપિંડીની ચેતવણી!"), + ("scam_text1", "જો તમે અજાણી વ્યક્તિ સાથે વાત કરી રહ્યા હો અને તેણે RustDesk વાપરવા કહ્યું હોય, તો તરત ડિસ્કનેક્ટ કરો."), + ("scam_text2", "આ એક છેતરપિંડી હોઈ શકે છે. કોઈને પાસવર્ડ આપશો નહીં."), + ("Don't show again", "ફરીથી ના બતાવશો"), + ("I Agree", "હું સહમત છું"), + ("Decline", "અસ્વીકાર"), + ("Timeout in minutes", "મિનિટોમાં ટાઇમઆઉટ"), + ("auto_disconnect_option_tip", "નિષ્ક્રિયતા પર આપમેળે ડિસ્કનેક્ટ કરો"), + ("Connection failed due to inactivity", "નિષ્ક્રિયતાને કારણે કનેક્શન નિષ્ફળ"), + ("Check for software update on startup", "શરૂઆતમાં અપડેટ તપાસો"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "સર્વર પ્રો ને {} માં અપગ્રેડ કરો"), + ("pull_group_failed_tip", "ગ્રુપ ખેંચવામાં (Pull) નિષ્ફળ"), + ("Filter by intersection", "ઇન્ટરસેક્શન દ્વારા ફિલ્ટર કરો"), + ("Remove wallpaper during incoming sessions", "કનેક્શન દરમિયાન વોલપેપર હટાવો"), + ("Test", "ટેસ્ટ"), + ("display_is_plugged_out_msg", "ડિસ્પ્લે કાઢી નાખવામાં આવ્યું છે."), + ("No displays", "કોઈ ડિસ્પ્લે નથી"), + ("Open in new window", "નવી વિન્ડોમાં ખોલો"), + ("Show displays as individual windows", "દરેક ડિસ્પ્લે અલગ વિન્ડોમાં બતાવો"), + ("Use all my displays for the remote session", "તમામ ડિસ્પ્લેનો ઉપયોગ કરો"), + ("selinux_tip", "SELinux ઉપકરણ પર સક્ષમ છે."), + ("Change view", "વ્યુ બદલો"), + ("Big tiles", "મોટી ટાઇલ્સ"), + ("Small tiles", "નાની ટાઇલ્સ"), + ("List", "લિસ્ટ"), + ("Virtual display", "વર્ચ્યુઅલ ડિસ્પ્લે"), + ("Plug out all", "બધું કાઢી નાખો (Plug out)"), + ("True color (4:4:4)", "ટ્રુ કલર (4:4:4)"), + ("Enable blocking user input", "યુઝર ઇનપુટ બ્લોકિંગ સક્ષમ કરો"), + ("id_input_tip", "તમે ID, Alias અથવા IP એડ્રેસ દાખલ કરી શકો છો."), + ("privacy_mode_impl_mag_tip", "મેગ્નિફાયર પ્રાઇવસી મોડ"), + ("privacy_mode_impl_virtual_display_tip", "વર્ચ્યુઅલ ડિસ્પ્લે પ્રાઇવસી મોડ"), + ("Enter privacy mode", "પ્રાઇવસી મોડમાં પ્રવેશ કરો"), + ("Exit privacy mode", "પ્રાઇવસી મોડમાંથી બહાર નીકળો"), + ("idd_not_support_under_win10_2004_tip", "વર્ચ્યુઅલ ડિસ્પ્લે Windows 10 (2004) કે તેથી ઉપર જ સક્ષમ છે."), + ("input_source_1_tip", "ઇનપુટ સ્ત્રોત ૧"), + ("input_source_2_tip", "ઇનપુટ સ્ત્રોત ૨"), + ("Swap control-command key", "Control અને Command કી બદલો"), + ("swap-left-right-mouse", "ડાબું અને જમણું માઉસ બટન બદલો"), + ("2FA code", "2FA કોડ"), + ("More", "વધારે"), + ("enable-2fa-title", "2FA સક્ષમ કરો"), + ("enable-2fa-desc", "તમારું ઓથેન્ટિકેટર એપ સેટ કરો."), + ("wrong-2fa-code", "ખોટો 2FA કોડ."), + ("enter-2fa-title", "2FA કોડ દાખલ કરો"), + ("Email verification code must be 6 characters.", "ઇમેઇલ કોડ 6 અક્ષરનો હોવો જોઈએ."), + ("2FA code must be 6 digits.", "2FA કોડ 6 અંકનો હોવો જોઈએ."), + ("Multiple Windows sessions found", "બહુવિધ Windows સત્રો મળ્યા"), + ("Please select the session you want to connect to", "કૃપા કરીને જે સત્ર સાથે જોડાવું હોય તે પસંદ કરો"), + ("powered_by_me", "મારા દ્વારા સંચાલિત"), + ("outgoing_only_desk_tip", "આ માત્ર આઉટગોઇંગ મોડ છે"), + ("preset_password_warning", "સુરક્ષા માટે પાસવર્ડ બદલો."), + ("Security Alert", "સુરક્ષા ચેતવણી"), + ("My address book", "મારી એડ્રેસ બુક"), + ("Personal", "વ્યક્તિગત"), + ("Owner", "માલિક"), + ("Set shared password", "શેર કરેલ પાસવર્ડ સેટ કરો"), + ("Exist in", "માં અસ્તિત્વ ધરાવે છે"), + ("Read-only", "માત્ર વાંચવા માટે"), + ("Read/Write", "વાંચવા/લખવા માટે"), + ("Full Control", "પૂર્ણ નિયંત્રણ"), + ("share_warning_tip", "તમે તમારો એક્સેસ શેર કરી રહ્યા છો."), + ("Everyone", "દરેક વ્યક્તિ"), + ("ab_web_console_tip", "વેબ કન્સોલ એડ્રેસ બુક"), + ("allow-only-conn-window-open-tip", "માત્ર RustDesk વિન્ડો ખુલ્લી હોય ત્યારે જ કનેક્શનની મંજૂરી આપો"), + ("no_need_privacy_mode_no_physical_displays_tip", "ભૌતિક ડિસ્પ્લે નથી, પ્રાઇવસી મોડની જરૂર નથી."), + ("Follow remote cursor", "રિમોટ કર્સરને અનુસરો"), + ("Follow remote window focus", "રિમોટ વિન્ડો ફોકસને અનુસરો"), + ("default_proxy_tip", "ડિફોલ્ટ પ્રોક્સી સેટિંગ"), + ("no_audio_input_device_tip", "કોઈ ઓડિયો ઇનપુટ મળ્યું નથી."), + ("Incoming", "આવતું"), + ("Outgoing", "જતું"), + ("Clear Wayland screen selection", "Wayland સ્ક્રીન સિલેક્શન સાફ કરો"), + ("clear_Wayland_screen_selection_tip", "સ્ક્રીન સિલેક્શન રીસેટ કરો."), + ("confirm_clear_Wayland_screen_selection_tip", "શું તમે સિલેક્શન સાફ કરવા માંગો છો?"), + ("android_new_voice_call_tip", "નવો વોઇસ કોલ વિનંતી"), + ("texture_render_tip", "ટેક્સચર રેન્ડરિંગ વાપરો"), + ("Use texture rendering", "ટેક્સચર રેન્ડરિંગનો ઉપયોગ કરો"), + ("Floating window", "ફ્લોટિંગ વિન્ડો"), + ("floating_window_tip", "બેકગ્રાઉન્ડમાં હોય ત્યારે RustDesk બતાવો"), + ("Keep screen on", "સ્ક્રીન ચાલુ રાખો"), + ("Never", "ક્યારેય નહીં"), + ("During controlled", "નિયંત્રણ દરમિયાન"), + ("During service is on", "જ્યારે સેવા ચાલુ હોય ત્યારે"), + ("Capture screen using DirectX", "DirectX દ્વારા સ્ક્રીન કેપ્ચર કરો"), + ("Back", "પાછળ"), + ("Apps", "એપ્સ"), + ("Volume up", "અવાજ વધારો"), + ("Volume down", "અવાજ ઘટાડો"), + ("Power", "પાવર"), + ("Telegram bot", "Telegram બોટ"), + ("enable-bot-tip", "સૂચનાઓ માટે બોટ સક્ષમ કરો"), + ("enable-bot-desc", "સૂચનાઓ માટે ટેલિગ્રામ બોટ સેટ કરો."), + ("cancel-2fa-confirm-tip", "શું તમે 2FA રદ કરવા માંગો છો?"), + ("cancel-bot-confirm-tip", "શું તમે બોટ રદ કરવા માંગો છો?"), + ("About RustDesk", "RustDesk વિશે"), + ("Send clipboard keystrokes", "ક્લિપબોર્ડ કી-સ્ટ્રોક્સ મોકલો"), + ("network_error_tip", "નેટવર્ક ભૂલ, ફરી પ્રયાસ કરો."), + ("Unlock with PIN", "PIN થી અનલોક કરો"), + ("Requires at least {} characters", "ઓછામાં ઓછા {} અક્ષર જરૂરી"), + ("Wrong PIN", "ખોટો PIN"), + ("Set PIN", "PIN સેટ કરો"), + ("Enable trusted devices", "વિશ્વાસપાત્ર ઉપકરણો સક્ષમ કરો"), + ("Manage trusted devices", "વિશ્વાસપાત્ર ઉપકરણો સંચાલિત કરો"), + ("Platform", "પ્લેટફોર્મ"), + ("Days remaining", "બાકી દિવસો"), + ("enable-trusted-devices-tip", "માત્ર વિશ્વાસપાત્ર ઉપકરણો જ પાસવર્ડ વગર જોડાઈ શકે"), + ("Parent directory", "પેરન્ટ ડિરેક્ટરી"), + ("Resume", "ફરી શરૂ કરો"), + ("Invalid file name", "અમાન્ય ફાઇલ નામ"), + ("one-way-file-transfer-tip", "માત્ર એકતરફી ફાઇલ ટ્રાન્સફરની મંજૂરી છે"), + ("Authentication Required", "ઓથેન્ટિકેશન જરૂરી"), + ("Authenticate", "ઓથેન્ટિકેટ કરો"), + ("web_id_input_tip", "રિમોટ ID દાખલ કરો"), + ("Download", "ડાઉનલોડ"), + ("Upload folder", "ફોલ્ડર અપલોડ કરો"), + ("Upload files", "ફાઇલો અપલોડ કરો"), + ("Clipboard is synchronized", "ક્લિપબોર્ડ સિંક થયેલ છે"), + ("Update client clipboard", "ક્લાયન્ટ ક્લિપબોર્ડ અપડેટ કરો"), + ("Untagged", "ટેગ વગરનું"), + ("new-version-of-{}-tip", "{} નું નવું વર્ઝન ઉપલબ્ધ છે"), + ("Accessible devices", "એક્સેસિબલ ઉપકરણો"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "રિમોટ ક્લાયન્ટને {} માં અપગ્રેડ કરો"), + ("d3d_render_tip", "D3D રેન્ડરિંગ વાપરો"), + ("Printer", "પ્રિન્ટર"), + ("printer-os-requirement-tip", "પ્રિન્ટિંગ માટે Windows જરૂરી છે."), + ("printer-requires-installed-{}-client-tip", "આ માટે {} ક્લાયન્ટ ઇન્સ્ટોલ હોવું જોઈએ."), + ("printer-{}-not-installed-tip", "પ્રિન્ટર {} ઇન્સ્ટોલ નથી."), + ("printer-{}-ready-tip", "પ્રિન્ટર {} તૈયાર છે."), + ("Install {} Printer", "{} પ્રિન્ટર ઇન્સ્ટોલ કરો"), + ("Outgoing Print Jobs", "જતા પ્રિન્ટ કાર્યો"), + ("Incoming Print Jobs", "આવતા પ્રિન્ટ કાર્યો"), + ("Incoming Print Job", "આવતું પ્રિન્ટ કાર્ય"), + ("use-the-default-printer-tip", "ડિફોલ્ટ પ્રિન્ટર વાપરો"), + ("use-the-selected-printer-tip", "પસંદ કરેલ પ્રિન્ટર વાપરો"), + ("auto-print-tip", "આપમેળે પ્રિન્ટ કરો"), + ("print-incoming-job-confirm-tip", "પ્રિન્ટ કરતા પહેલા પુષ્ટિ કરો"), + ("remote-printing-disallowed-tile-tip", "રિમોટ પ્રિન્ટિંગની મંજૂરી નથી"), + ("remote-printing-disallowed-text-tip", "સેટિંગ્સમાં રિમોટ પ્રિન્ટિંગ સક્ષમ કરો."), + ("save-settings-tip", "સેટિંગ્સ સાચવો"), + ("dont-show-again-tip", "ફરીથી ના બતાવશો"), + ("Take screenshot", "સ્ક્રીનશોટ લો"), + ("Taking screenshot", "સ્ક્રીનશોટ લેવાઈ રહ્યો છે"), + ("screenshot-merged-screen-not-supported-tip", "મર્જ કરેલ સ્ક્રીનશોટ સપોર્ટેડ નથી."), + ("screenshot-action-tip", "સ્ક્રીનશોટ પછીની ક્રિયા"), + ("Save as", "તરીકે સાચવો"), + ("Copy to clipboard", "ક્લિપબોર્ડમાં કોપી કરો"), + ("Enable remote printer", "રિમોટ પ્રિન્ટર સક્ષમ કરો"), + ("Downloading {}", "{} ડાઉનલોડ થઈ રહ્યું છે"), + ("{} Update", "{} અપડેટ"), + ("{}-to-update-tip", "અપડેટ કરવા માટે {}"), + ("download-new-version-failed-tip", "નવું વર્ઝન ડાઉનલોડ કરવામાં નિષ્ફળ."), + ("Auto update", "ઓટો અપડેટ"), + ("update-failed-check-msi-tip", "અપડેટ નિષ્ફળ, MSI ફાઇલ તપાસો."), + ("websocket_tip", "જો પોર્ટ બ્લોક હોય તો WebSocket વાપરો."), + ("Use WebSocket", "WebSocket નો ઉપયોગ કરો"), + ("Trackpad speed", "ટ્રેકપેડ સ્પીડ"), + ("Default trackpad speed", "ડિફોલ્ટ ટ્રેકપેડ સ્પીડ"), + ("Numeric one-time password", "ન્યુમેરિક OTP"), + ("Enable IPv6 P2P connection", "IPv6 P2P કનેક્શન સક્ષમ કરો"), + ("Enable UDP hole punching", "UDP હોલ પંચિંગ સક્ષમ કરો"), + ("View camera", "કેમેરા જુઓ"), + ("Enable camera", "કેમેરા સક્ષમ કરો"), + ("No cameras", "કોઈ કેમેરા મળ્યો નથી"), + ("view_camera_unsupported_tip", "રિમોટ કેમેરા સપોર્ટેડ નથી."), + ("Terminal", "ટર્મિનલ"), + ("Enable terminal", "ટર્મિનલ સક્ષમ કરો"), + ("New tab", "નવી ટેબ"), + ("Keep terminal sessions on disconnect", "ડિસ્કનેક્ટ વખતે ટર્મિનલ ચાલુ રાખો"), + ("Terminal (Run as administrator)", "ટર્મિનલ (એડમિનિસ્ટ્રેટર તરીકે)"), + ("terminal-admin-login-tip", "એડમિન લોગિન જરૂરી છે."), + ("Failed to get user token.", "યુઝર ટોકન મેળવવામાં નિષ્ફળ."), + ("Incorrect username or password.", "ખોટું યુઝરનેમ કે પાસવર્ડ."), + ("The user is not an administrator.", "યુઝર એડમિનિસ્ટ્રેટર નથી."), + ("Failed to check if the user is an administrator.", "યુઝર એડમિન છે કે નહીં તે ચકાસવામાં નિષ્ફળ."), + ("Supported only in the installed version.", "માત્ર ઇન્સ્ટોલ કરેલ વર્ઝનમાં ઉપલબ્ધ."), + ("elevation_username_tip", "એડમિનિસ્ટ્રેટર નામ દાખલ કરો"), + ("Preparing for installation ...", "ઇન્સ્ટોલેશનની તૈયારી..."), + ("Show my cursor", "મારું કર્સર બતાવો"), + ("Scale custom", "કસ્ટમ સ્કેલ"), + ("Custom scale slider", "કસ્ટમ સ્કેલ સ્લાઇડર"), + ("Decrease", "ઘટાડો"), + ("Increase", "વધારો"), + ("Show virtual mouse", "વર્ચ્યુઅલ માઉસ બતાવો"), + ("Virtual mouse size", "વર્ચ્યુઅલ માઉસ કદ"), + ("Small", "નાનું"), + ("Large", "મોટું"), + ("Show virtual joystick", "વર્ચ્યુઅલ જોયસ્ટિક બતાવો"), + ("Edit note", "નોંધ સુધારો"), + ("Alias", "Alias (ઉપનામ)"), + ("ScrollEdge", "સ્ક્રોલ એજ"), + ("Allow insecure TLS fallback", "અસુરક્ષિત TLS ફોલબેકની મંજૂરી આપો"), + ("allow-insecure-tls-fallback-tip", "જૂના સર્વર માટે વાપરો."), + ("Disable UDP", "UDP અક્ષમ કરો"), + ("disable-udp-tip", "કનેક્શન સમસ્યાઓ માટે UDP બંધ કરો."), + ("server-oss-not-support-tip", "OSS સર્વર આને સપોર્ટ કરતું નથી."), + ("input note here", "અહીં નોંધ લખો"), + ("note-at-conn-end-tip", "કનેક્શનના અંતે નોંધ બતાવો"), + ("Show terminal extra keys", "ટર્મિનલની વધારાની કી બતાવો"), + ("Relative mouse mode", "રીલેટિવ માઉસ મોડ"), + ("rel-mouse-not-supported-peer-tip", "સામેથી સપોર્ટેડ નથી."), + ("rel-mouse-not-ready-tip", "તૈયાર નથી."), + ("rel-mouse-lock-failed-tip", "માઉસ લોક નિષ્ફળ."), + ("rel-mouse-exit-{}-tip", "બહાર નીકળવા {} દબાવો"), + ("rel-mouse-permission-lost-tip", "પરવાનગી ગુમાવી દીધી."), + ("Changelog", "Changelog (ફેરફારો)"), + ("keep-awake-during-outgoing-sessions-label", "આઉટગોઇંગ સત્ર વખતે જાગૃત રાખો"), + ("keep-awake-during-incoming-sessions-label", "ઇનકમિંગ સત્ર વખતે જાગૃત રાખો"), + ("Continue with {}", "{} સાથે આગળ વધો"), + ("Display Name", "ડિસ્પ્લે નામ"), + ("password-hidden-tip", "સુરક્ષા માટે પાસવર્ડ છુપાવેલ છે."), + ("preset-password-in-use-tip", "પ્રીસેટ પાસવર્ડ વપરાશમાં છે."), + ].iter().cloned().collect(); +} From 28e303576c4a589b13cc5a008dc35f5c53bce543 Mon Sep 17 00:00:00 2001 From: Leo Louis Date: Tue, 14 Apr 2026 11:51:27 +0530 Subject: [PATCH 210/277] Add support for Gujarati language in lang.rs (#14751) --- src/lang.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lang.rs b/src/lang.rs index 4c49c48ca..85ae23c9c 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -16,6 +16,7 @@ mod es; mod et; mod eu; mod fa; +mod gu; mod fr; mod he; mod hr; @@ -95,6 +96,7 @@ pub const LANGS: &[(&str, &str)] = &[ ("ta", "தமிழ்"), ("ge", "ქართული"), ("fi", "Suomi"), + ("gu", "ગુજરાતી"), ]; #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -173,6 +175,7 @@ pub fn translate_locale(name: String, locale: &str) -> String { "sc" => sc::T.deref(), "ta" => ta::T.deref(), "ge" => ge::T.deref(), + "gu" => gu::T.deref(), _ => en::T.deref(), }; let (name, placeholder_value) = extract_placeholder(&name); From 68fa0466c88826d27b0d0282d170a679af30b0f0 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 15 Apr 2026 14:36:03 +0800 Subject: [PATCH 211/277] improved oidc login error --- flutter/lib/common/widgets/login.dart | 66 +++++++++++++++++++++------ 1 file changed, 52 insertions(+), 14 deletions(-) diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart index 62ade8e51..1cca69285 100644 --- a/flutter/lib/common/widgets/login.dart +++ b/flutter/lib/common/widgets/login.dart @@ -224,21 +224,59 @@ class _WidgetOPState extends State { return Offstage( offstage: _failedMsg.isEmpty && widget.curOP.value != widget.config.op, - child: RichText( - text: TextSpan( - text: '$_stateMsg ', - style: - DefaultTextStyle.of(context).style.copyWith(fontSize: 12), - children: [ - TextSpan( - text: _failedMsg, - style: DefaultTextStyle.of(context).style.copyWith( - fontSize: 14, - color: Colors.red, - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (_stateMsg.isNotEmpty && _failedMsg.isEmpty) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: SelectableText( + translate(_stateMsg), + style: DefaultTextStyle.of(context) + .style + .copyWith(fontSize: 12), + ), ), - ], - ), + if (_failedMsg.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Builder(builder: (context) { + final errorColor = + Theme.of(context).colorScheme.error; + final bgColor = Theme.of(context) + .colorScheme + .errorContainer + .withOpacity(0.3); + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, vertical: 6.0), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(4.0), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.error_outline, + color: errorColor, size: 16), + const SizedBox(width: 6), + Flexible( + child: SelectableText( + translate(_failedMsg), + style: DefaultTextStyle.of(context) + .style + .copyWith( + fontSize: 13, + color: errorColor, + ), + ), + ), + ], + ), + ); + }), + ), + ], ), ); }), From 91de51290df044b08351f0801db8d1efb552933d Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 15 Apr 2026 14:39:46 +0800 Subject: [PATCH 212/277] add microsoft oidc logo --- flutter/assets/auth-microsoft.svg | 1 + flutter/lib/common/widgets/login.dart | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 flutter/assets/auth-microsoft.svg diff --git a/flutter/assets/auth-microsoft.svg b/flutter/assets/auth-microsoft.svg new file mode 100644 index 000000000..c9ce5f9cf --- /dev/null +++ b/flutter/assets/auth-microsoft.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart index 1cca69285..ee376de68 100644 --- a/flutter/lib/common/widgets/login.dart +++ b/flutter/lib/common/widgets/login.dart @@ -20,7 +20,8 @@ const kOpSvgList = [ 'okta', 'facebook', 'azure', - 'auth0' + 'auth0', + 'microsoft' ]; class _IconOP extends StatelessWidget { From 091f2c6135e575c4252ea37fa8545779c4570132 Mon Sep 17 00:00:00 2001 From: pallab-js Date: Wed, 15 Apr 2026 15:05:51 +0530 Subject: [PATCH 213/277] impl(cm): implement change_theme and change_language callbacks (#14782) * docs: fix typos in documentation and code comments - Fix 'seperated' -> 'separated' in remote_input.dart - Fix 'seperators' -> 'separators' in fuse/cs.rs - Update outdated 'OSX' -> 'macOS' in virtual display README Signed-off-by: pallab-js * impl(cm): implement change_theme and change_language callbacks These callbacks were previously empty TODO stubs. Now they properly invoke the Sciter UI handlers to notify the UI when theme or language changes occur. Signed-off-by: pallab-js --------- Signed-off-by: pallab-js --- flutter/lib/common/widgets/remote_input.dart | 2 +- libs/clipboard/src/platform/unix/fuse/cs.rs | 2 +- libs/virtual_display/dylib/README.md | 2 +- src/ui/cm.rs | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index e35da6424..5871033db 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -31,7 +31,7 @@ class RawKeyFocusScope extends StatelessWidget { // https://github.com/flutter/flutter/issues/154053 final useRawKeyEvents = isLinux && !isWeb; // FIXME: On Windows, `AltGr` will generate `Alt` and `Control` key events, - // while `Alt` and `Control` are seperated key events for en-US input method. + // while `Alt` and `Control` are separated key events for en-US input method. return FocusScope( autofocus: true, child: Focus( diff --git a/libs/clipboard/src/platform/unix/fuse/cs.rs b/libs/clipboard/src/platform/unix/fuse/cs.rs index 0f1cf8739..fa1dea71d 100644 --- a/libs/clipboard/src/platform/unix/fuse/cs.rs +++ b/libs/clipboard/src/platform/unix/fuse/cs.rs @@ -12,7 +12,7 @@ //! //! For now, we transfer all file names with windows separators, UTF-16 encoded. //! *Need a way to transfer file names with '\' safely*. -//! Maybe we can use URL encoded file names and '/' seperators as a new standard, while keep the support to old schemes. +//! Maybe we can use URL encoded file names and '/' separators as a new standard, while keep the support to old schemes. //! //! # Note //! - all files on FS should be read only, and mark the owner to be the current user diff --git a/libs/virtual_display/dylib/README.md b/libs/virtual_display/dylib/README.md index 30fa588f1..fb71c3c56 100644 --- a/libs/virtual_display/dylib/README.md +++ b/libs/virtual_display/dylib/README.md @@ -29,4 +29,4 @@ TODO ## X11 -## OSX +## macOS diff --git a/src/ui/cm.rs b/src/ui/cm.rs index 15b7b9435..8eb8f494e 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -52,12 +52,12 @@ impl InvokeUiCM for SciterHandler { self.call("newMessage", &make_args!(id, text)); } - fn change_theme(&self, _dark: String) { - // TODO + fn change_theme(&self, dark: String) { + self.call("changeTheme", &make_args!(dark)); } fn change_language(&self) { - // TODO + self.call("changeLanguage", &make_args!()); } fn show_elevation(&self, show: bool) { From 9f817714fe76d40604c46832c96e6f741a549818 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 15 Apr 2026 21:40:03 +0800 Subject: [PATCH 214/277] fix(client): stop retrying on restricted mobile access errors (#14797) Treat "Access to mobile devices is restricted in your country" as a non-retriable connection error so the error dialog does not trigger reconnect attempts. Signed-off-by: 21pages --- src/client.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client.rs b/src/client.rs index 527f65a12..72652776a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -3870,6 +3870,7 @@ pub fn check_if_retry(msgtype: &str, title: &str, text: &str, retry_for_relay: b && !text.to_lowercase().contains("resolve") && !text.to_lowercase().contains("mismatch") && !text.to_lowercase().contains("manually") + && !text.to_lowercase().contains("restricted") && !text.to_lowercase().contains("not allowed"))) } From 1e9c4d04f164941812d4e23271ea063313299976 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Thu, 16 Apr 2026 23:21:14 +0800 Subject: [PATCH 215/277] fix(mobile): deeplink, disable by default (#14824) Signed-off-by: fufesou --- flutter/lib/common.dart | 25 +++++++++++++++++++++++++ flutter/lib/consts.dart | 3 +++ libs/hbb_common | 2 +- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index ad3bbc9f6..e579db36a 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -2365,6 +2365,19 @@ List? urlLinkToCmdArgs(Uri uri) { id = uri.path.substring("/new/".length); } else if (uri.authority == "config") { if (isAndroid || isIOS) { + final allowDeepLinkServerSettings = + bind.mainGetBuildinOption(key: kOptionAllowDeepLinkServerSettings) == + 'Y'; + if (!allowDeepLinkServerSettings) { + debugPrint( + "Ignore rustdesk://config because $kOptionAllowDeepLinkServerSettings is not enabled."); + // Keep the user-facing error generic; detailed rejection reason is in debug logs. + // Delay toast to avoid missing overlay during cold-start deeplink handling. + Timer(Duration(seconds: 1), () { + showToast(translate('Failed')); + }); + return null; + } final config = uri.path.substring("/".length); // add a timer to make showToast work Timer(Duration(seconds: 1), () { @@ -2374,6 +2387,18 @@ List? urlLinkToCmdArgs(Uri uri) { return null; } else if (uri.authority == "password") { if (isAndroid || isIOS) { + final allowDeepLinkPassword = + bind.mainGetBuildinOption(key: kOptionAllowDeepLinkPassword) == 'Y'; + if (!allowDeepLinkPassword) { + debugPrint( + "Ignore rustdesk://password because $kOptionAllowDeepLinkPassword is not enabled."); + // Keep the user-facing error generic; detailed rejection reason is in debug logs. + // Delay toast to avoid missing overlay during cold-start deeplink handling. + Timer(Duration(seconds: 1), () { + showToast(translate('Failed')); + }); + return null; + } final password = uri.path.substring("/".length); if (password.isNotEmpty) { Timer(Duration(seconds: 1), () async { diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index b1112dd29..51c08cf33 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -187,6 +187,9 @@ const String kOptionDisableChangeId = "disable-change-id"; const String kOptionDisableUnlockPin = "disable-unlock-pin"; const kHideUsernameOnCard = "hide-username-on-card"; const String kOptionHideHelpCards = "hide-help-cards"; +const String kOptionAllowDeepLinkPassword = "allow-deep-link-password"; +const String kOptionAllowDeepLinkServerSettings = + "allow-deep-link-server-settings"; const String kOptionToggleViewOnly = "view-only"; const String kOptionToggleShowMyCursor = "show-my-cursor"; diff --git a/libs/hbb_common b/libs/hbb_common index 618922b2a..87b11a795 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 618922b2a77f7be44fc7b86e41f6cfba87d62193 +Subproject commit 87b11a795964b00deded250657a63626f2c1efa0 From 642c281ad015296c55e501bbc6aaf3c56a26ff68 Mon Sep 17 00:00:00 2001 From: John Fowler Date: Fri, 17 Apr 2026 06:44:24 +0200 Subject: [PATCH 216/277] Update hu.rs (#14816) New string translation and fixes. --- src/lang/hu.rs | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/lang/hu.rs b/src/lang/hu.rs index e69514e45..2ba49a0cf 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -57,7 +57,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ID Server", "ID-kiszolgáló"), ("Relay Server", "Továbbító-kiszolgáló"), ("API Server", "API-kiszolgáló"), - ("invalid_http", "A címnek mindenképpen http(s)://-el kell kezdődnie."), + ("invalid_http", "A címnek mindenképpen http(s)://-rel kell kezdődnie."), ("Invalid IP", "A megadott IP-cím érvénytelen"), ("Invalid format", "Érvénytelen formátum"), ("server_not_support", "A kiszolgáló nem támogatja"), @@ -149,7 +149,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Click to upgrade", "Kattintson ide a frissítés telepítéséhez"), ("Configure", "Beállítás"), ("config_acc", "A számítógép távoli vezérléséhez a RustDesknek hozzáférési jogokat kell adnia."), - ("config_screen", "Ahhoz, hogy távolról hozzáférhessen a számítógépéhez, meg kell adnia a RustDesknek a \"Képernyőfelvétel\" jogosultságot."), + ("config_screen", "Ahhoz, hogy távolról hozzáférhessen a számítógépéhez, meg kell adnia a RustDesknek a „Képernyőfelvétel” jogosultságot."), ("Installing ...", "Telepítés ..."), ("Install", "Telepítse"), ("Installation", "Telepítés"), @@ -276,13 +276,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Do you accept?", "Elfogadás?"), ("Open System Setting", "Rendszerbeállítások megnyitása"), ("How to get Android input permission?", "Hogyan állítható be az Androidos beviteli engedély?"), - ("android_input_permission_tip1", "Ahhoz, hogy egy távoli eszköz vezérelhesse Android készülékét, engedélyeznie kell a RustDesk számára a \"Hozzáférhetőség\" szolgáltatás használatát."), - ("android_input_permission_tip2", "A következő rendszerbeállítások oldalon a letöltött alkalmazások menüponton belül, kapcsolja be a [RustDesk Input] szolgáltatást."), + ("android_input_permission_tip1", "Ahhoz, hogy egy távoli eszköz vezérelhesse Android készülékét, engedélyeznie kell a RustDesk számára a „Hozzáférhetőség” szolgáltatás használatát."), + ("android_input_permission_tip2", "A következő rendszerbeállítások oldalon a letöltött alkalmazások menüponton belül, kapcsolja be a „RustDesk Input” szolgáltatást."), ("android_new_connection_tip", "Új kérés érkezett, mely vezérelni szeretné az eszközét"), ("android_service_will_start_tip", "A képernyőmegosztás aktiválása automatikusan elindítja a szolgáltatást, így más eszközök is vezérelhetik ezt az Android-eszközt."), ("android_stop_service_tip", "A szolgáltatás leállítása automatikusan szétkapcsol minden létező kapcsolatot."), ("android_version_audio_tip", "A jelenlegi Android verzió nem támogatja a hangrögzítést, frissítsen legalább Android 10-re, vagy egy újabb verzióra."), - ("android_start_service_tip", "A képernyőmegosztó szolgáltatás elindításához koppintson a \"Kapcsolási szolgáltatás indítása\" gombra, vagy aktiválja a \"Képernyőfelvétel\" engedélyt."), + ("android_start_service_tip", "A képernyőmegosztó szolgáltatás elindításához koppintson a „Kapcsolási szolgáltatás indítása” gombra, vagy aktiválja a „Képernyőfelvétel” engedélyt."), ("android_permission_may_not_change_tip", "A meglévő kapcsolatok engedélyei csak új kapcsolódás után módosulnak."), ("Account", "Fiók"), ("Overwrite", "Felülírás"), @@ -408,15 +408,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", "Helyi billentyűzet típusának kiválasztása"), ("software_render_tip", "Ha Nvidia grafikus kártyát használ Linux alatt, és a távoli ablak a kapcsolat létrehozása után azonnal bezáródik, akkor a Nouveau nyílt forráskódú illesztőprogramra való váltás és a szoftveres leképezés alkalmazása segíthet. A szoftvert újra kell indítani."), ("Always use software rendering", "Mindig szoftveres leképezést használjon"), - ("config_input", "Ahhoz, hogy a távoli asztalt a billentyűzettel vezérelhesse, a RustDesknek meg kell adnia a \"Bemenet figyelése\" jogosultságot."), - ("config_microphone", "Ahhoz, hogy távolról beszélhessen, meg kell adnia a RustDesknek a \"Hangfelvétel\" jogosultságot."), + ("config_input", "Ahhoz, hogy a távoli asztalt a billentyűzettel vezérelhesse, a RustDesknek meg kell adnia a „Bemenet figyelése” jogosultságot."), + ("config_microphone", "Ahhoz, hogy távolról beszélhessen, meg kell adnia a RustDesknek a „Hangfelvétel” jogosultságot."), ("request_elevation_tip", "Akkor is kérhet megnövelt jogokat, ha valaki a partneroldalon van."), ("Wait", "Várjon"), ("Elevation Error", "Emelt szintű hozzáférési hiba"), ("Ask the remote user for authentication", "Hitelesítés kérése a távoli felhasználótól"), ("Choose this if the remote account is administrator", "Akkor válassza ezt, ha a távoli fiók rendszergazda"), ("Transmit the username and password of administrator", "Küldje el a rendszergazda felhasználónevét és jelszavát"), - ("still_click_uac_tip", "A távoli felhasználónak továbbra is az \"Igen\" gombra kell kattintania a RustDesk UAC ablakában. Kattintson!"), + ("still_click_uac_tip", "A távoli felhasználónak továbbra is az „Igen” gombra kell kattintania a RustDesk UAC ablakában. Kattintson!"), ("Request Elevation", "Emelt szintű jogok igénylése"), ("wait_accept_uac_tip", "Várjon, amíg a távoli felhasználó elfogadja az UAC párbeszédet."), ("Elevate successfully", "Emelt szintű jogok megadva"), @@ -442,7 +442,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Hanghívás"), ("Text chat", "Szöveges csevegés"), ("Stop voice call", "Hanghívás leállítása"), - ("relay_hint_tip", "Ha a közvetlen kapcsolat nem lehetséges, megpróbálhat kapcsolatot létesíteni egy továbbító-kiszolgálón keresztül.\nHa az első próbálkozáskor továbbító-kiszolgálón keresztüli kapcsolatot szeretne létrehozni, használhatja az \"/r\" utótagot. Az azonosítóhoz vagy a \"Mindig továbbító-kiszolgálón keresztül kapcsolódom\" opcióhoz a legutóbbi munkamenetek listájában, ha van ilyen."), + ("relay_hint_tip", "Ha a közvetlen kapcsolat nem lehetséges, megpróbálhat kapcsolatot létesíteni egy továbbító-kiszolgálón keresztül.\nHa az első próbálkozáskor továbbító-kiszolgálón keresztüli kapcsolatot szeretne létrehozni, használhatja az „/r” utótagot. Az azonosítóhoz vagy a „Mindig továbbító-kiszolgálón keresztül kapcsolódom” opcióhoz a legutóbbi munkamenetek listájában, ha van ilyen."), ("Reconnect", "Újrakapcsolódás"), ("Codec", "Kodek"), ("Resolution", "Felbontás"), @@ -559,7 +559,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Plug out all", "Kapcsolja ki az összeset"), ("True color (4:4:4)", "Valódi szín (4:4:4)"), ("Enable blocking user input", "Engedélyezze a felhasználói bevitel blokkolását"), - ("id_input_tip", "Megadhat egy azonosítót, egy közvetlen IP-címet vagy egy tartományt egy porttal (:).\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (@?key=), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a \"@public\" lehetőséget. A kulcsra nincs szükség nyilvános kiszolgálók esetén.\n\nHa az első kapcsolathoz továbbító-kiszolgálón keresztüli kapcsolatot akar kényszeríteni, adja hozzá az \"/r\" az azonosítót a végén, például \"9123456234/r\"."), + ("id_input_tip", "Megadhat egy azonosítót, egy közvetlen IP-címet vagy egy tartományt egy porttal (:).\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (@?key=), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a „@public” lehetőséget. A kulcsra nincs szükség nyilvános kiszolgálók esetén.\n\nHa az első kapcsolathoz továbbító-kiszolgálón keresztüli kapcsolatot akar kényszeríteni, adja hozzá az „/r” az azonosítót a végén, például „9123456234/r”."), ("privacy_mode_impl_mag_tip", "1. mód"), ("privacy_mode_impl_virtual_display_tip", "2. mód"), ("Enter privacy mode", "Lépjen be az adatvédelmi módba"), @@ -622,7 +622,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Power", "Főkapcsoló"), ("Telegram bot", "Telegram bot"), ("enable-bot-tip", "Ha aktiválja ezt a funkciót, akkor a 2FA-kódot a botjától kaphatja meg. Kapcsolati értesítésként is használható."), - ("enable-bot-desc", "1. Nyisson csevegést @BotFather.\n2. Küldje el a \"/newbot\" parancsot. Miután ezt a lépést elvégezte, kap egy tokent.\n3. Indítson csevegést az újonnan létrehozott botjával. Küldjön egy olyan üzenetet, amely egy perjel (\"/\") kezdetű, pl. \"/hello\" az aktiváláshoz.\n"), + ("enable-bot-desc", "1. Nyisson csevegést @BotFather.\n2. Küldje el a „/newbot” parancsot. Miután ezt a lépést elvégezte, kap egy tokent.\n3. Indítson csevegést az újonnan létrehozott botjával. Küldjön egy olyan üzenetet, amely egy perjel („/”) kezdetű, pl. „/hello” az aktiváláshoz.\n"), ("cancel-2fa-confirm-tip", "Biztosan vissza akarja vonni a 2FA-hitelesítést?"), ("cancel-bot-confirm-tip", "Biztosan le akarja mondani a Telegram botot?"), ("About RustDesk", "A RustDesk névjegye"), @@ -643,7 +643,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", "Az egyirányú fájlátvitel engedélyezve van a vezérelt oldalon."), ("Authentication Required", "Hitelesítés szükséges"), ("Authenticate", "Hitelesítés"), - ("web_id_input_tip", "Azonos kiszolgálón lévő azonosítót adhat meg, a közvetlen IP elérés nem támogatott a webkliensben.\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (@?key=), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a \"@public\" betűt. A kulcsra nincs szükség a nyilvános kiszolgálók esetében."), + ("web_id_input_tip", "Azonos kiszolgálón lévő azonosítót adhat meg, a közvetlen IP elérés nem támogatott a webkliensben.\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (@?key=), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg az „@public” kulcsot. A kulcsra nincs szükség a nyilvános kiszolgálók esetében."), ("Download", "Letöltés"), ("Upload folder", "Mappa feltöltése"), ("Upload files", "Fájlok feltöltése"), @@ -682,9 +682,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Downloading {}", "{} letöltése"), ("{} Update", "{} frissítés"), ("{}-to-update-tip", "{} bezárása és az új verzió telepítése."), - ("download-new-version-failed-tip", "Ha a letöltés sikertelen, akkor vagy újrapróbálkozhat, vagy a \"Letöltés\" gombra kattintva letöltheti a kiadási oldalról, és manuálisan frissíthet."), + ("download-new-version-failed-tip", "Ha a letöltés sikertelen, akkor vagy újrapróbálkozhat, vagy a „Letöltés” gombra kattintva letöltheti a kiadási oldalról, és manuálisan frissíthet."), ("Auto update", "Automatikus frissítés"), - ("update-failed-check-msi-tip", "A telepítési módszer felismerése nem sikerült. Kattintson a \"Letöltés\" gombra, hogy letöltse a kiadási oldalról, és manuálisan frissítse."), + ("update-failed-check-msi-tip", "A telepítési módszer felismerése nem sikerült. Kattintson a „Letöltés” gombra, hogy letöltse a kiadási oldalról, és manuálisan frissítse."), ("websocket_tip", "WebSocket használatakor csak a relé-kapcsolatok támogatottak."), ("Use WebSocket", "WebSocket használata"), ("Trackpad speed", "Érintőpad sebessége"), @@ -741,7 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Képernyő aktív állapotban tartása a bejövő munkamenetek során"), ("Continue with {}", "Folytatás ezzel: {}"), ("Display Name", "Kijelző név"), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), + ("password-hidden-tip", "Állandó jelszó lett beállítva (rejtett)."), + ("preset-password-in-use-tip", "Jelenleg az alapértelmezett jelszót használja."), ].iter().cloned().collect(); } From 91aff3ffd1597adda98493aa7816f56fe9d5c9ab Mon Sep 17 00:00:00 2001 From: Luca-rickrolled-himself <88965309+LucaBarbaLata@users.noreply.github.com> Date: Sat, 18 Apr 2026 05:55:18 +0300 Subject: [PATCH 217/277] Complete and correct Romanian (ro) translations (#14837) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Complete and correct Romanian (ro) translations - Fill in all previously empty translation strings - Fix plural form: "fișier" → "fișiere" (files) - Fix "Receive" → "Primește" (was incorrectly using "Acceptă") - Fix "Too frequent" → "Prea frecvent" (removed erroneous extra word) - Fix "Note" → "Notă" (was translated as verb instead of noun) - Fix "Use both passwords" → "Folosește ambele parole" ("programe" typo) - Fix "Automatically record incoming sessions" → "sesiunile primite" (not "viitoare") - Fix typo "neautoriztă" → "neautorizată" (Connection not allowed) - Fix typo "dispozivul" → "dispozitivul" (Restart remote device) - Fix leading whitespace in "Username" translation - Fix "FPS" → keep as "FPS" (was incorrectly translated as "CPS") - Fix "Forget Password" → "Parolă uitată" (command form was grammatically wrong) * Fix typo in Romanian translation for accessibility tip * unify informal register and fix subjunctive typo --- src/lang/ro.rs | 504 ++++++++++++++++++++++++------------------------- 1 file changed, 252 insertions(+), 252 deletions(-) diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 0a5ab0299..797bae8f7 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -62,7 +62,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Invalid format", "Format nevalid"), ("server_not_support", "Încă nu este compatibil cu serverul"), ("Not available", "Indisponibil"), - ("Too frequent", "Modificat prea frecvent"), + ("Too frequent", "Prea frecvent"), ("Cancel", "Anulează"), ("Skip", "Omite"), ("Close", "Închide"), @@ -87,7 +87,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Modified", "Modificat"), ("Size", "Dimensiune"), ("Show Hidden Files", "Afișează fișiere ascunse"), - ("Receive", "Acceptă"), + ("Receive", "Primește"), ("Send", "Trimite"), ("Refresh File", "Actualizează fișier"), ("Local", "Local"), @@ -108,7 +108,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Do this for all conflicts", "Aplică la toate conflictele"), ("This is irreversible!", "Această acțiune este ireversibilă!"), ("Deleting", "În curs de ștergere..."), - ("files", "fișier"), + ("files", "fișiere"), ("Waiting", "În așteptare..."), ("Finished", "Finalizat"), ("Speed", "Viteză"), @@ -203,7 +203,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("x11 expected", "Este necesar X11"), ("Port", "Port"), ("Settings", "Setări"), - ("Username", " Nume utilizator"), + ("Username", "Nume utilizator"), ("Invalid port", "Port nevalid"), ("Closed manually by the peer", "Conexiune închisă manual de dispozitivul pereche"), ("Enable remote configuration modification", "Activează modificarea configurației de la distanță"), @@ -216,7 +216,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remember me", "Reține-mă"), ("Trust this device", "Acest dispozitiv este de încredere"), ("Verification code", "Cod de verificare"), - ("verification_tip", ""), + ("verification_tip", "Introdu codul de verificare trimis la adresa ta de e-mail sau generat de aplicația de autentificare."), ("Logout", "Deconectează-te"), ("Tags", "Etichete"), ("Search ID", "Caută după ID"), @@ -228,9 +228,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Username missed", "Lipsește numele de utilizator"), ("Password missed", "Lipsește parola"), ("Wrong credentials", "Nume sau parolă greșită"), - ("The verification code is incorrect or has expired", ""), + ("The verification code is incorrect or has expired", "Codul de verificare este incorect sau a expirat"), ("Edit Tag", "Modifică etichetă"), - ("Forget Password", "Uită parola"), + ("Forget Password", "Parolă uitată"), ("Favorites", "Favorite"), ("Add to Favorites", "Adaugă la Favorite"), ("Remove from Favorites", "Șterge din Favorite"), @@ -263,7 +263,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Canvas Zoom", "Mărire ecran"), ("Reset canvas", "Reinițializează ecranul"), ("No permission of file transfer", "Nicio permisiune pentru transferul de fișiere"), - ("Note", "Reține"), + ("Note", "Notă"), ("Connection", "Conexiune"), ("Share screen", "Partajează ecran"), ("Chat", "Mesaje"), @@ -276,14 +276,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Do you accept?", "Accepți?"), ("Open System Setting", "Deschide setări sistem"), ("How to get Android input permission?", "Cum autorizez dispozitive de intrare pe Android?"), - ("android_input_permission_tip1", "Pentru ca un dispozitiv la distanță să poată controla un dispozitiv Android folosind mouse-ul sau suportul tactil, trebuie să permiți RustDesk să utilize serviciul „Accesibilitate”."), + ("android_input_permission_tip1", "Pentru ca un dispozitiv la distanță să poată controla un dispozitiv Android folosind mouse-ul sau suportul tactil, trebuie să permiți RustDesk să utilizeze serviciul „Accesibilitate"."), ("android_input_permission_tip2", "Accesează următoarea pagină din Setări, deschide [Aplicații instalate] și pornește serviciul [RustDesk Input]."), ("android_new_connection_tip", "Ai primit o nouă solicitare de controlare a dispozitivului actual."), ("android_service_will_start_tip", "Activarea setării de capturare a ecranului va porni automat serviciul, permițând altor dispozitive să solicite conectarea la dispozitivul tău."), ("android_stop_service_tip", "Închiderea serviciului va închide automat toate conexiunile stabilite."), ("android_version_audio_tip", "Versiunea actuală de Android nu suportă captura audio. Fă upgrade la Android 10 sau la o versiune superioară."), ("android_start_service_tip", "Apasă [Pornește serviciu] sau DESCHIDE [Capturare ecran] pentru a porni serviciul de partajare a ecranului."), - ("android_permission_may_not_change_tip", ""), + ("android_permission_may_not_change_tip", "Este posibil ca unele permisiuni să nu poată fi modificate în funcție de versiunea de Android."), ("Account", "Cont"), ("Overwrite", "Suprascrie"), ("This file exists, skip or overwrite this file?", "Fișier deja existent. Omite sau suprascrie?"), @@ -304,15 +304,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_open_battery_optimizations_tip", "Pentru dezactivarea acestei funcții, accesează setările aplicației RustDesk, deschide secțiunea [Baterie] și deselectează [Fără restricții]."), ("Start on boot", "Pornește la boot"), ("Start the screen sharing service on boot, requires special permissions", "Pornește serviciul de partajare a ecranului la boot; necesită permisiuni speciale"), - ("Connection not allowed", "Conexiune neautoriztă"), + ("Connection not allowed", "Conexiune neautorizată"), ("Legacy mode", "Mod legacy"), ("Map mode", "Mod hartă"), ("Translate mode", "Mod traducere"), ("Use permanent password", "Folosește parola permanentă"), - ("Use both passwords", "Folosește ambele programe"), + ("Use both passwords", "Folosește ambele parole"), ("Set permanent password", "Setează parola permanentă"), ("Enable remote restart", "Activează repornirea la distanță"), - ("Restart remote device", "Repornește dispozivul la distanță"), + ("Restart remote device", "Repornește dispozitivul la distanță"), ("Are you sure you want to restart", "Sigur vrei să repornești dispozitivul?"), ("Restarting remote device", "Se repornește dispozitivul la distanță"), ("remote_restarting_tip", "Dispozitivul este în curs de repornire. Închide acest mesaj și reconectează-te cu parola permanentă după un timp."), @@ -359,8 +359,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unpin Toolbar", "Detașează bara de instrumente"), ("Recording", "Înregistrare"), ("Directory", "Director"), - ("Automatically record incoming sessions", "Înregistrează automat sesiunile viitoare"), - ("Automatically record outgoing sessions", ""), + ("Automatically record incoming sessions", "Înregistrează automat sesiunile primite"), + ("Automatically record outgoing sessions", "Înregistrează automat sesiunile de ieșire"), ("Change", "Modifică"), ("Start session recording", "Începe înregistrarea"), ("Stop session recording", "Oprește înregistrarea"), @@ -379,7 +379,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Share", "Partajare ecran"), ("ubuntu-21-04-required", "Wayland necesită Ubuntu 21.04 sau o versiune superioară."), ("wayland-requires-higher-linux-version", "Wayland necesită o versiune superioară a distribuției Linux. Încearcă desktopul X11 sau schimbă sistemul de operare."), - ("xdp-portal-unavailable", ""), + ("xdp-portal-unavailable", "Portalul XDG Desktop nu este disponibil. Asigură-te că rulezi o sesiune Wayland cu suport pentru portal."), ("JumpLink", "Afișează"), ("Please Select the screen to be shared(Operate on the peer side).", "Partajează ecranul care urmează să fie partajat (operează din partea dispozitivului pereche)."), ("Show RustDesk", "Afișează RustDesk"), @@ -436,13 +436,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Default Image Quality", "Calitatea implicită a imaginii"), ("Default Codec", "Codec implicit"), ("Bitrate", "Rată de biți"), - ("FPS", "CPS"), + ("FPS", "FPS"), ("Auto", "Auto"), ("Other Default Options", "Alte opțiuni implicite"), ("Voice call", "Apel vocal"), ("Text chat", "Conversație text"), ("Stop voice call", "Încheie apel vocal"), - ("relay_hint_tip", "Este posibil să nu te poți conecta direct; poți încerca să te conectezi prin retransmisie. De asemenea, dacă dorești să te conectezi direct prin retransmisie, poți adăuga sufixul „/r” la ID sau să bifezi opțiunea Conectează-te mereu prin retransmisie."), + ("relay_hint_tip", "Este posibil să nu te poți conecta direct; poți încerca să te conectezi prin retransmisie. De asemenea, dacă dorești să te conectezi direct prin retransmisie, poți adăuga sufixul „/r" la ID sau să bifezi opțiunea Conectează-te mereu prin retransmisie."), ("Reconnect", "Reconectează-te"), ("Codec", "Codec"), ("Resolution", "Rezoluție"), @@ -503,245 +503,245 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Exit", "Ieși"), ("Open", "Deschide"), ("logout_tip", "Sigur vrei să te deconectezi?"), - ("Service", ""), - ("Start", ""), - ("Stop", ""), - ("exceed_max_devices", ""), - ("Sync with recent sessions", ""), - ("Sort tags", ""), - ("Open connection in new tab", ""), - ("Move tab to new window", ""), - ("Can not be empty", ""), - ("Already exists", ""), - ("Change Password", ""), - ("Refresh Password", ""), - ("ID", ""), - ("Grid View", ""), - ("List View", ""), - ("Select", ""), - ("Toggle Tags", ""), - ("pull_ab_failed_tip", ""), - ("push_ab_failed_tip", ""), - ("synced_peer_readded_tip", ""), - ("Change Color", ""), - ("Primary Color", ""), - ("HSV Color", ""), - ("Installation Successful!", ""), - ("Installation failed!", ""), - ("Reverse mouse wheel", ""), - ("{} sessions", ""), - ("scam_title", ""), - ("scam_text1", ""), - ("scam_text2", ""), - ("Don't show again", ""), - ("I Agree", ""), - ("Decline", ""), - ("Timeout in minutes", ""), - ("auto_disconnect_option_tip", ""), - ("Connection failed due to inactivity", ""), - ("Check for software update on startup", ""), - ("upgrade_rustdesk_server_pro_to_{}_tip", ""), - ("pull_group_failed_tip", ""), - ("Filter by intersection", ""), - ("Remove wallpaper during incoming sessions", ""), - ("Test", ""), - ("display_is_plugged_out_msg", ""), - ("No displays", ""), - ("Open in new window", ""), - ("Show displays as individual windows", ""), - ("Use all my displays for the remote session", ""), - ("selinux_tip", ""), - ("Change view", ""), - ("Big tiles", ""), - ("Small tiles", ""), - ("List", ""), - ("Virtual display", ""), - ("Plug out all", ""), - ("True color (4:4:4)", ""), - ("Enable blocking user input", ""), - ("id_input_tip", ""), - ("privacy_mode_impl_mag_tip", ""), - ("privacy_mode_impl_virtual_display_tip", ""), - ("Enter privacy mode", ""), - ("Exit privacy mode", ""), - ("idd_not_support_under_win10_2004_tip", ""), - ("input_source_1_tip", ""), - ("input_source_2_tip", ""), - ("Swap control-command key", ""), - ("swap-left-right-mouse", ""), - ("2FA code", ""), - ("More", ""), - ("enable-2fa-title", ""), - ("enable-2fa-desc", ""), - ("wrong-2fa-code", ""), - ("enter-2fa-title", ""), - ("Email verification code must be 6 characters.", ""), - ("2FA code must be 6 digits.", ""), - ("Multiple Windows sessions found", ""), - ("Please select the session you want to connect to", ""), - ("powered_by_me", ""), - ("outgoing_only_desk_tip", ""), - ("preset_password_warning", ""), - ("Security Alert", ""), - ("My address book", ""), - ("Personal", ""), - ("Owner", ""), - ("Set shared password", ""), - ("Exist in", ""), - ("Read-only", ""), - ("Read/Write", ""), - ("Full Control", ""), - ("share_warning_tip", ""), - ("Everyone", ""), - ("ab_web_console_tip", ""), - ("allow-only-conn-window-open-tip", ""), - ("no_need_privacy_mode_no_physical_displays_tip", ""), - ("Follow remote cursor", ""), - ("Follow remote window focus", ""), - ("default_proxy_tip", ""), - ("no_audio_input_device_tip", ""), - ("Incoming", ""), - ("Outgoing", ""), - ("Clear Wayland screen selection", ""), - ("clear_Wayland_screen_selection_tip", ""), - ("confirm_clear_Wayland_screen_selection_tip", ""), - ("android_new_voice_call_tip", ""), - ("texture_render_tip", ""), - ("Use texture rendering", ""), - ("Floating window", ""), - ("floating_window_tip", ""), - ("Keep screen on", ""), - ("Never", ""), - ("During controlled", ""), - ("During service is on", ""), - ("Capture screen using DirectX", ""), - ("Back", ""), - ("Apps", ""), - ("Volume up", ""), - ("Volume down", ""), - ("Power", ""), - ("Telegram bot", ""), - ("enable-bot-tip", ""), - ("enable-bot-desc", ""), - ("cancel-2fa-confirm-tip", ""), - ("cancel-bot-confirm-tip", ""), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), - ("one-way-file-transfer-tip", ""), - ("Authentication Required", ""), - ("Authenticate", ""), - ("web_id_input_tip", ""), - ("Download", ""), - ("Upload folder", ""), - ("Upload files", ""), - ("Clipboard is synchronized", ""), - ("Update client clipboard", ""), - ("Untagged", ""), - ("new-version-of-{}-tip", ""), - ("Accessible devices", ""), - ("upgrade_remote_rustdesk_client_to_{}_tip", ""), - ("d3d_render_tip", ""), - ("Use D3D rendering", ""), - ("Printer", ""), - ("printer-os-requirement-tip", ""), - ("printer-requires-installed-{}-client-tip", ""), - ("printer-{}-not-installed-tip", ""), - ("printer-{}-ready-tip", ""), - ("Install {} Printer", ""), - ("Outgoing Print Jobs", ""), - ("Incoming Print Jobs", ""), - ("Incoming Print Job", ""), - ("use-the-default-printer-tip", ""), - ("use-the-selected-printer-tip", ""), - ("auto-print-tip", ""), - ("print-incoming-job-confirm-tip", ""), - ("remote-printing-disallowed-tile-tip", ""), - ("remote-printing-disallowed-text-tip", ""), - ("save-settings-tip", ""), - ("dont-show-again-tip", ""), - ("Take screenshot", ""), - ("Taking screenshot", ""), - ("screenshot-merged-screen-not-supported-tip", ""), - ("screenshot-action-tip", ""), - ("Save as", ""), - ("Copy to clipboard", ""), - ("Enable remote printer", ""), - ("Downloading {}", ""), - ("{} Update", ""), - ("{}-to-update-tip", ""), - ("download-new-version-failed-tip", ""), - ("Auto update", ""), - ("update-failed-check-msi-tip", ""), - ("websocket_tip", ""), - ("Use WebSocket", ""), - ("Trackpad speed", ""), - ("Default trackpad speed", ""), - ("Numeric one-time password", ""), - ("Enable IPv6 P2P connection", ""), - ("Enable UDP hole punching", ""), + ("Service", "Serviciu"), + ("Start", "Pornește"), + ("Stop", "Oprește"), + ("exceed_max_devices", "Numărul maxim de dispozitive a fost depășit"), + ("Sync with recent sessions", "Sincronizează cu sesiunile recente"), + ("Sort tags", "Sortează etichete"), + ("Open connection in new tab", "Deschide conexiunea într-o filă nouă"), + ("Move tab to new window", "Mută fila într-o fereastră nouă"), + ("Can not be empty", "Nu poate fi gol"), + ("Already exists", "Există deja"), + ("Change Password", "Schimbă parola"), + ("Refresh Password", "Reîmprospătează parola"), + ("ID", "ID"), + ("Grid View", "Vizualizare grilă"), + ("List View", "Vizualizare listă"), + ("Select", "Selectează"), + ("Toggle Tags", "Comută etichete"), + ("pull_ab_failed_tip", "Sincronizarea agendei a eșuat. Verifică conexiunea la rețea sau autentifică-te din nou."), + ("push_ab_failed_tip", "Salvarea agendei pe server a eșuat. Verifică conexiunea la rețea sau autentifică-te din nou."), + ("synced_peer_readded_tip", "Dispozitivele pereche eliminate au fost re-adăugate automat din sesiunile recente."), + ("Change Color", "Schimbă culoarea"), + ("Primary Color", "Culoare principală"), + ("HSV Color", "Culoare HSV"), + ("Installation Successful!", "Instalare reușită!"), + ("Installation failed!", "Instalare eșuată!"), + ("Reverse mouse wheel", "Inversează rotiță mouse"), + ("{} sessions", "{} sesiuni"), + ("scam_title", "Avertisment de securitate"), + ("scam_text1", "Escrocii se pot da drept angajați ai asistenței tehnice și îți pot solicita să instalezi sau să rulezi RustDesk pentru a-ți accesa dispozitivul."), + ("scam_text2", "Dacă nu ai contactat tu primul asistența tehnică, te rugăm să închizi această aplicație imediat."), + ("Don't show again", "Nu mai afișa"), + ("I Agree", "Sunt de acord"), + ("Decline", "Refuză"), + ("Timeout in minutes", "Timp de expirare în minute"), + ("auto_disconnect_option_tip", "Deconectează automat sesiunile de la distanță după o perioadă de inactivitate."), + ("Connection failed due to inactivity", "Conexiunea a eșuat din cauza inactivității"), + ("Check for software update on startup", "Verifică actualizări la pornire"), + ("upgrade_rustdesk_server_pro_{}_tip", "Versiunea serverului RustDesk Pro este mai mică decât {}. Te rugăm să o actualizezi."), + ("pull_group_failed_tip", "Sincronizarea grupului a eșuat. Verifică conexiunea la rețea sau autentifică-te din nou."), + ("Filter by intersection", "Filtrează prin intersecție"), + ("Remove wallpaper during incoming sessions", "Elimină imaginea de fundal în timpul sesiunilor primite"), + ("Test", "Test"), + ("display_is_plugged_out_msg", "Monitorul selectat a fost deconectat. Sesiunea continuă pe monitorul disponibil."), + ("No displays", "Niciun monitor"), + ("Open in new window", "Deschide în fereastră nouă"), + ("Show displays as individual windows", "Afișează monitoarele ca ferestre individuale"), + ("Use all my displays for the remote session", "Folosește toate monitoarele mele pentru sesiunea la distanță"), + ("selinux_tip", "SELinux este activat pe acest sistem. Este posibil ca unele funcții să nu funcționeze corect. Te rugăm să consulți documentația pentru instrucțiuni de configurare."), + ("Change view", "Schimbă vizualizarea"), + ("Big tiles", "Dale mari"), + ("Small tiles", "Dale mici"), + ("List", "Listă"), + ("Virtual display", "Monitor virtual"), + ("Plug out all", "Deconectează toate"), + ("True color (4:4:4)", "Culori reale (4:4:4)"), + ("Enable blocking user input", "Activează blocarea intrărilor utilizatorului"), + ("id_input_tip", "Introdu ID-ul sau adresa IP a dispozitivului la distanță"), + ("privacy_mode_impl_mag_tip", "Modul privat prin Magnificare — nu este suportat pe toate sistemele"), + ("privacy_mode_impl_virtual_display_tip", "Modul privat prin monitor virtual — necesită driverul de monitor virtual"), + ("Enter privacy mode", "Intră în modul privat"), + ("Exit privacy mode", "Ieși din modul privat"), + ("idd_not_support_under_win10_2004_tip", "Driverul de monitor virtual nu este suportat pe versiuni de Windows anterioare versiunii 2004 (build 19041)."), + ("input_source_1_tip", "Sursă de intrare 1 — folosește metodele standard de simulare a tastaturii și mouse-ului"), + ("input_source_2_tip", "Sursă de intrare 2 — folosește driver-ul RustDesk pentru simulare la nivel de kernel"), + ("Swap control-command key", "Schimbă tastele Control și Command"), + ("swap-left-right-mouse", "Schimbă butoanele stâng și drept ale mouse-ului"), + ("2FA code", "Cod 2FA"), + ("More", "Mai mult"), + ("enable-2fa-title", "Activează autentificarea în doi pași (2FA)"), + ("enable-2fa-desc", "Scanează codul QR cu o aplicație de autentificare (de ex. Google Authenticator) și introdu codul generat pentru a confirma activarea."), + ("wrong-2fa-code", "Cod 2FA incorect"), + ("enter-2fa-title", "Introdu codul de autentificare în doi pași"), + ("Email verification code must be 6 characters.", "Codul de verificare prin e-mail trebuie să aibă 6 caractere."), + ("2FA code must be 6 digits.", "Codul 2FA trebuie să conțină 6 cifre."), + ("Multiple Windows sessions found", "Au fost găsite mai multe sesiuni Windows"), + ("Please select the session you want to connect to", "Selectează sesiunea la care vrei să te conectezi"), + ("powered_by_me", "Realizat cu RustDesk"), + ("outgoing_only_desk_tip", "Acest dispozitiv este configurat doar pentru conexiuni de ieșire și nu acceptă conexiuni de intrare."), + ("preset_password_warning", "Parola prestabilită nu este recomandată din motive de securitate. Te rugăm să o schimbi cât mai curând posibil."), + ("Security Alert", "Alertă de securitate"), + ("My address book", "Agenda mea"), + ("Personal", "Personal"), + ("Owner", "Proprietar"), + ("Set shared password", "Setează parola partajată"), + ("Exist in", "Există în"), + ("Read-only", "Doar citire"), + ("Read/Write", "Citire/Scriere"), + ("Full Control", "Control total"), + ("share_warning_tip", "Datele partajate vor fi vizibile pentru toți membrii grupului selectat. Asigură-te că partajezi doar informații adecvate."), + ("Everyone", "Toată lumea"), + ("ab_web_console_tip", "Gestionează agenda prin consola web RustDesk Pro."), + ("allow-only-conn-window-open-tip", "Permite conexiunile numai atunci când fereastra de gestionare a conexiunilor este deschisă"), + ("no_need_privacy_mode_no_physical_displays_tip", "Modul privat nu este necesar deoarece nu există monitoare fizice conectate."), + ("Follow remote cursor", "Urmărește cursorul de la distanță"), + ("Follow remote window focus", "Urmărește fereastra activă de la distanță"), + ("default_proxy_tip", "Proxy-ul implicit este utilizat pentru toate conexiunile dacă nu este specificat altul."), + ("no_audio_input_device_tip", "Nu a fost găsit niciun dispozitiv de intrare audio. Conectează un microfon și reîncearcă."), + ("Incoming", "Intrare"), + ("Outgoing", "Ieșire"), + ("Clear Wayland screen selection", "Șterge selecția de ecran Wayland"), + ("clear_Wayland_screen_selection_tip", "Șterge selecția de ecran Wayland salvată, astfel încât să poți alege un alt ecran la următoarea conexiune."), + ("confirm_clear_Wayland_screen_selection_tip", "Sigur vrei să ștergi selecția de ecran Wayland?"), + ("android_new_voice_call_tip", "Ai primit un nou apel vocal. Apasă pentru a accepta sau respinge."), + ("texture_render_tip", "Randarea prin textură poate îmbunătăți performanța grafică pe unele dispozitive. Repornește aplicația dacă apar probleme de afișare."), + ("Use texture rendering", "Folosește randarea prin textură"), + ("Floating window", "Fereastră flotantă"), + ("floating_window_tip", "Fereastra flotantă ajută la menținerea serviciului de partajare a ecranului activ în fundal pe Android."), + ("Keep screen on", "Menține ecranul pornit"), + ("Never", "Niciodată"), + ("During controlled", "În timpul controlului"), + ("During service is on", "Cât timp serviciul este activ"), + ("Capture screen using DirectX", "Capturează ecranul folosind DirectX"), + ("Back", "Înapoi"), + ("Apps", "Aplicații"), + ("Volume up", "Mărește volumul"), + ("Volume down", "Micșorează volumul"), + ("Power", "Alimentare"), + ("Telegram bot", "Bot Telegram"), + ("enable-bot-tip", "Activează botul Telegram pentru a primi notificări și a gestiona conexiunile."), + ("enable-bot-desc", "Configurează un bot Telegram pentru notificări RustDesk. Introdu token-ul botului și ID-ul chat-ului."), + ("cancel-2fa-confirm-tip", "Sigur vrei să dezactivezi autentificarea în doi pași? Aceasta va reduce securitatea contului tău."), + ("cancel-bot-confirm-tip", "Sigur vrei să dezactivezi botul Telegram?"), + ("About RustDesk", "Despre RustDesk"), + ("Send clipboard keystrokes", "Trimite conținutul clipboard-ului ca apăsări de taste"), + ("network_error_tip", "Eroare de rețea. Verifică conexiunea la internet și încearcă din nou."), + ("Unlock with PIN", "Deblochează cu PIN"), + ("Requires at least {} characters", "Necesită cel puțin {} caractere"), + ("Wrong PIN", "PIN incorect"), + ("Set PIN", "Setează PIN"), + ("Enable trusted devices", "Activează dispozitive de încredere"), + ("Manage trusted devices", "Gestionează dispozitivele de încredere"), + ("Platform", "Platformă"), + ("Days remaining", "Zile rămase"), + ("enable-trusted-devices-tip", "Dispozitivele de încredere pot accesa contul fără verificare suplimentară."), + ("Parent directory", "Director părinte"), + ("Resume", "Reia"), + ("Invalid file name", "Nume de fișier nevalid"), + ("one-way-file-transfer-tip", "Transferul de fișiere în sens unic permite doar trimiterea sau primirea de fișiere, nu ambele direcții simultan."), + ("Authentication Required", "Autentificare necesară"), + ("Authenticate", "Autentifică-te"), + ("web_id_input_tip", "Introdu ID-ul RustDesk al dispozitivului la care vrei să te conectezi"), + ("Download", "Descarcă"), + ("Upload folder", "Încarcă folder"), + ("Upload files", "Încarcă fișiere"), + ("Clipboard is synchronized", "Clipboard-ul este sincronizat"), + ("Update client clipboard", "Actualizează clipboard-ul clientului"), + ("Untagged", "Neetichetat"), + ("new-version-of-{}-tip", "Este disponibilă o nouă versiune a {}. Fă clic pentru a actualiza."), + ("Accessible devices", "Dispozitive accesibile"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Versiunea clientului RustDesk de la distanță este mai mică decât {}. Te rugăm să o actualizezi pentru o compatibilitate completă."), + ("d3d_render_tip", "Randarea Direct3D poate îmbunătăți performanța pe sistemele Windows cu suport hardware adecvat."), + ("Use D3D rendering", "Folosește randarea D3D"), + ("Printer", "Imprimantă"), + ("printer-os-requirement-tip", "Imprimarea la distanță necesită Windows 10 sau o versiune superioară."), + ("printer-requires-installed-{}-client-tip", "Imprimarea la distanță necesită instalarea clientului {} pe dispozitivul local."), + ("printer-{}-not-installed-tip", "Imprimanta {} nu este instalată. Instalează driverul imprimantei pentru a continua."), + ("printer-{}-ready-tip", "Imprimanta {} este pregătită pentru utilizare."), + ("Install {} Printer", "Instalează imprimanta {}"), + ("Outgoing Print Jobs", "Lucrări de imprimare de ieșire"), + ("Incoming Print Jobs", "Lucrări de imprimare de intrare"), + ("Incoming Print Job", "Lucrare de imprimare de intrare"), + ("use-the-default-printer-tip", "Folosește imprimanta implicită a sistemului pentru lucrările de imprimare primite."), + ("use-the-selected-printer-tip", "Folosește imprimanta selectată pentru lucrările de imprimare primite."), + ("auto-print-tip", "Imprimă automat lucrările primite fără confirmare."), + ("print-incoming-job-confirm-tip", "Ai primit o lucrare de imprimare. Vrei să o imprimești?"), + ("remote-printing-disallowed-tile-tip", "Imprimare la distanță nepermisă"), + ("remote-printing-disallowed-text-tip", "Dispozitivul la distanță nu permite imprimarea. Contactează administratorul pentru a activa această funcție."), + ("save-settings-tip", "Salvează setările curente ca implicite pentru sesiunile viitoare."), + ("dont-show-again-tip", "Nu mai afișa acest mesaj"), + ("Take screenshot", "Fă captură de ecran"), + ("Taking screenshot", "Se face captura de ecran..."), + ("screenshot-merged-screen-not-supported-tip", "Captura de ecran a ecranului combinat nu este suportată în prezent."), + ("screenshot-action-tip", "Selectează acțiunea pentru captura de ecran: salvează ca fișier sau copiază în clipboard."), + ("Save as", "Salvează ca"), + ("Copy to clipboard", "Copiază în clipboard"), + ("Enable remote printer", "Activează imprimanta la distanță"), + ("Downloading {}", "Se descarcă {}"), + ("{} Update", "Actualizare {}"), + ("{}-to-update-tip", "Este disponibilă o actualizare pentru {}. Fă clic pentru a descărca și instala."), + ("download-new-version-failed-tip", "Descărcarea noii versiuni a eșuat. Verifică conexiunea la internet și încearcă din nou."), + ("Auto update", "Actualizare automată"), + ("update-failed-check-msi-tip", "Actualizarea a eșuat. Încearcă să descarci și să instalezi manual fișierul MSI."), + ("websocket_tip", "WebSocket oferă o conexiune mai stabilă în unele medii de rețea restrictive."), + ("Use WebSocket", "Folosește WebSocket"), + ("Trackpad speed", "Viteza touchpad-ului"), + ("Default trackpad speed", "Viteza implicită a touchpad-ului"), + ("Numeric one-time password", "Parolă unică numerică"), + ("Enable IPv6 P2P connection", "Activează conexiunea P2P prin IPv6"), + ("Enable UDP hole punching", "Activează traversarea UDP (hole punching)"), ("View camera", "Vezi camera"), - ("Enable camera", ""), - ("No cameras", ""), - ("view_camera_unsupported_tip", ""), - ("Terminal", ""), - ("Enable terminal", ""), - ("New tab", ""), - ("Keep terminal sessions on disconnect", ""), - ("Terminal (Run as administrator)", ""), - ("terminal-admin-login-tip", ""), - ("Failed to get user token.", ""), - ("Incorrect username or password.", ""), - ("The user is not an administrator.", ""), - ("Failed to check if the user is an administrator.", ""), - ("Supported only in the installed version.", ""), - ("elevation_username_tip", ""), - ("Preparing for installation ...", ""), - ("Show my cursor", ""), + ("Enable camera", "Activează camera"), + ("No cameras", "Nicio cameră disponibilă"), + ("view_camera_unsupported_tip", "Vizualizarea camerei nu este suportată pe dispozitivul la distanță."), + ("Terminal", "Terminal"), + ("Enable terminal", "Activează terminalul"), + ("New tab", "Filă nouă"), + ("Keep terminal sessions on disconnect", "Păstrează sesiunile de terminal la deconectare"), + ("Terminal (Run as administrator)", "Terminal (Rulează ca administrator)"), + ("terminal-admin-login-tip", "Introdu datele de autentificare ale administratorului pentru a rula terminalul cu privilegii sporite."), + ("Failed to get user token.", "Obținerea tokenului de utilizator a eșuat."), + ("Incorrect username or password.", "Nume de utilizator sau parolă incorectă."), + ("The user is not an administrator.", "Utilizatorul nu este administrator."), + ("Failed to check if the user is an administrator.", "Verificarea privilegiilor de administrator a eșuat."), + ("Supported only in the installed version.", "Suportat doar în versiunea instalată."), + ("elevation_username_tip", "Introdu numele de utilizator al contului de administrator pentru a solicita sporirea privilegiilor."), + ("Preparing for installation ...", "Se pregătește instalarea..."), + ("Show my cursor", "Afișează cursorul meu"), ("Scale custom", "Scalare personalizată"), ("Custom scale slider", "Glisor pentru scalare personalizată"), ("Decrease", "Micșorează"), ("Increase", "Mărește"), - ("Show virtual mouse", ""), - ("Virtual mouse size", ""), - ("Small", ""), - ("Large", ""), - ("Show virtual joystick", ""), - ("Edit note", ""), - ("Alias", ""), - ("ScrollEdge", ""), - ("Allow insecure TLS fallback", ""), - ("allow-insecure-tls-fallback-tip", ""), - ("Disable UDP", ""), - ("disable-udp-tip", ""), - ("server-oss-not-support-tip", ""), - ("input note here", ""), - ("note-at-conn-end-tip", ""), - ("Show terminal extra keys", ""), - ("Relative mouse mode", ""), - ("rel-mouse-not-supported-peer-tip", ""), - ("rel-mouse-not-ready-tip", ""), - ("rel-mouse-lock-failed-tip", ""), - ("rel-mouse-exit-{}-tip", ""), - ("rel-mouse-permission-lost-tip", ""), - ("Changelog", ""), - ("keep-awake-during-outgoing-sessions-label", ""), - ("keep-awake-during-incoming-sessions-label", ""), + ("Show virtual mouse", "Afișează mouse virtual"), + ("Virtual mouse size", "Dimensiunea mouse-ului virtual"), + ("Small", "Mic"), + ("Large", "Mare"), + ("Show virtual joystick", "Afișează joystick virtual"), + ("Edit note", "Editează notă"), + ("Alias", "Alias"), + ("ScrollEdge", "Derulare la margine"), + ("Allow insecure TLS fallback", "Permite revenirea la TLS nesecurizat"), + ("allow-insecure-tls-fallback-tip", "Permite conexiunile cu certificate TLS nevalide sau expirate. Nu este recomandat din motive de securitate."), + ("Disable UDP", "Dezactivează UDP"), + ("disable-udp-tip", "Dezactivează conexiunile UDP și folosește doar TCP. Poate reduce performanța conexiunii."), + ("server-oss-not-support-tip", "Serverul open-source nu suportă această funcție. Folosește RustDesk Pro pentru funcționalitate completă."), + ("input note here", "Introdu o notă aici"), + ("note-at-conn-end-tip", "Afișează această notă la sfârșitul sesiunii de conexiune."), + ("Show terminal extra keys", "Afișează taste suplimentare pentru terminal"), + ("Relative mouse mode", "Mod mouse relativ"), + ("rel-mouse-not-supported-peer-tip", "Dispozitivul pereche nu suportă modul mouse relativ."), + ("rel-mouse-not-ready-tip", "Modul mouse relativ nu este pregătit. Încearcă din nou."), + ("rel-mouse-lock-failed-tip", "Blocarea mouse-ului în modul relativ a eșuat."), + ("rel-mouse-exit-{}-tip", "Apasă {} pentru a ieși din modul mouse relativ."), + ("rel-mouse-permission-lost-tip", "Permisiunea pentru modul mouse relativ a fost pierdută."), + ("Changelog", "Jurnal de modificări"), + ("keep-awake-during-outgoing-sessions-label", "Menține ecranul activ în timpul sesiunilor de ieșire"), + ("keep-awake-during-incoming-sessions-label", "Menține ecranul activ în timpul sesiunilor de intrare"), ("Continue with {}", "Continuă cu {}"), - ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), + ("Display Name", "Nume afișat"), + ("password-hidden-tip", "Parola este ascunsă din motive de securitate. Fă clic pe pictograma ochiului pentru a o afișa."), + ("preset-password-in-use-tip", "Se folosește o parolă prestabilită. Se recomandă setarea unei parole personalizate pentru securitate sporită."), ].iter().cloned().collect(); } From ac124c068056395f9456a6c42eddab89b469a3a8 Mon Sep 17 00:00:00 2001 From: 21pages Date: Sat, 18 Apr 2026 11:19:32 +0800 Subject: [PATCH 218/277] flutter: improve address book pull error handling (#14813) * flutter: improve address book pull error handling Summary: - Show error messages when fetching the address book list fails. - After the initial fetch, switching back to the AB tab no longer re-fetches it, even if an error occurred or the error banner was dismissed. Tested: - Self-hosted server: - normal - 403 responses - legacy address book mode - Public server - Verified that switching tabs no longer re-fetches AB after the initial fetch, regardless of whether an error occurred or the error banner was cleared. Signed-off-by: 21pages * use resp.statusCode in address book json decoding Signed-off-by: 21pages * flutter: clear address book list errors on reset Signed-off-by: 21pages * flutter: clear address book pull errors consistently Signed-off-by: 21pages --------- Signed-off-by: 21pages --- flutter/lib/common/widgets/address_book.dart | 4 +- flutter/lib/models/ab_model.dart | 92 +++++++++++++++----- flutter/lib/models/group_model.dart | 1 + 3 files changed, 74 insertions(+), 23 deletions(-) diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index 1a09d6f53..054a1666c 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -54,9 +54,9 @@ class _AddressBookState extends State { const LinearProgressIndicator(), buildErrorBanner(context, loading: gFFI.abModel.currentAbLoading, - err: gFFI.abModel.currentAbPullError, + err: gFFI.abModel.abPullError, retry: null, - close: () => gFFI.abModel.currentAbPullError.value = ''), + close: gFFI.abModel.clearPullErrors), buildErrorBanner(context, loading: gFFI.abModel.currentAbLoading, err: gFFI.abModel.currentAbPushError, diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index 81c4dc851..001887c0c 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/hbbs/hbbs.dart'; @@ -53,7 +52,9 @@ class AbModel { RxBool get currentAbLoading => current.abLoading; bool get currentAbEmpty => current.peers.isEmpty && current.tags.isEmpty; - RxString get currentAbPullError => current.pullError; + final _listPullError = ''.obs; + RxString get abPullError => + _listPullError.value.isNotEmpty ? _listPullError : current.pullError; RxString get currentAbPushError => current.pushError; String? _personalAbGuid; RxBool legacyMode = false.obs; @@ -68,6 +69,7 @@ class AbModel { var _syncFromRecentLock = false; var _timerCounter = 0; var _cacheLoadOnceFlag = false; + var _pulledOnce = false; var listInitialized = false; var _maxPeerOneAb = 0; @@ -97,10 +99,17 @@ class AbModel { print("reset ab model"); addressbooks.clear(); _currentName.value = ''; + _listPullError.value = ''; + _pulledOnce = false; await bind.mainClearAb(); listInitialized = false; } + void clearPullErrors() { + _listPullError.value = ''; + current.pullError.value = ''; + } + // #region ab /// Pulls the address book data from the server. /// @@ -110,31 +119,41 @@ class AbModel { var _pulling = false; Future pullAb( {required ForcePullAb? force, required bool quiet}) async { + if (bind.isDisableAb()) return; + if (!gFFI.userModel.isLogin) return; + if (gFFI.userModel.networkError.isNotEmpty) return; if (_pulling) return; + if (force == null && _pulledOnce) { + return; + } _pulling = true; + if (!quiet) { + _listPullError.value = ''; + current.pullError.value = ''; + } try { await _pullAb(force: force, quiet: quiet); _refreshTab(); } catch (_) {} _pulling = false; + _pulledOnce = true; } Future _pullAb( {required ForcePullAb? force, required bool quiet}) async { - if (bind.isDisableAb()) return; - if (!gFFI.userModel.isLogin) return; - if (gFFI.userModel.networkError.isNotEmpty) return; if (force == null && listInitialized && current.initialized) return; debugPrint("pullAb, force: $force, quiet: $quiet"); if (!listInitialized || force == ForcePullAb.listAndCurrent) { try { // Read personal guid every time to avoid upgrading the server without closing the main window _personalAbGuid = null; - await _getPersonalAbGuid(); - // Determine legacy mode based on whether _personalAbGuid is null + // `true`: continue init. `false`: stop, error already recorded. + if (!await _getPersonalAbGuid(quiet: quiet)) { + return; + } legacyMode.value = _personalAbGuid == null; if (!legacyMode.value && _maxPeerOneAb == 0) { - await _getAbSettings(); + await _getAbSettings(quiet: quiet); } if (_personalAbGuid != null) { debugPrint("pull ab list"); @@ -142,7 +161,7 @@ class AbModel { abProfiles.add(AbProfile(_personalAbGuid!, _personalAddressBookName, gFFI.userModel.userName.value, null, ShareRule.read.value, null)); // get all address book name - await _getSharedAbProfiles(abProfiles); + await _getSharedAbProfiles(abProfiles, quiet: quiet); addressbooks.removeWhere((key, value) => abProfiles.firstWhereOrNull((e) => e.name == key) == null); for (int i = 0; i < abProfiles.length; i++) { @@ -182,6 +201,7 @@ class AbModel { } } catch (e) { debugPrint("pull ab list error: $e"); + _setListPullError(e, quiet: quiet); } } else if (listInitialized && (!current.initialized || force == ForcePullAb.current)) { @@ -197,14 +217,26 @@ class AbModel { } } - Future _getAbSettings() async { + void _setListPullError(Object err, {required bool quiet, int? statusCode}) { + if (!quiet) { + _listPullError.value = + '${translate('pull_ab_failed_tip')}: ${translate(err.toString())}'; + } + if (statusCode == 401) { + gFFI.userModel.reset(resetOther: true); + } + } + + Future _getAbSettings({required bool quiet}) async { + int? statusCode; try { final api = "${await bind.mainGetApiServer()}/api/ab/settings"; var headers = getHttpHeaders(); headers['Content-Type'] = "application/json"; _setEmptyBody(headers); final resp = await http.post(Uri.parse(api), headers: headers); - if (resp.statusCode == 404) { + statusCode = resp.statusCode; + if (statusCode == 404) { debugPrint("HTTP 404, api server doesn't support shared address book"); return false; } @@ -213,46 +245,57 @@ class AbModel { if (json.containsKey('error')) { throw json['error']; } - if (resp.statusCode != 200) { - throw 'HTTP ${resp.statusCode}'; + if (statusCode != 200) { + throw 'HTTP $statusCode'; } _maxPeerOneAb = json['max_peer_one_ab'] ?? 0; return true; } catch (err) { debugPrint('get ab settings err: ${err.toString()}'); + _setListPullError(err, quiet: quiet, statusCode: statusCode); } return false; } - Future _getPersonalAbGuid() async { + /// Loads `/api/ab/personal`. + /// Returns `true` to continue init, `false` to stop after a real error. + Future _getPersonalAbGuid({required bool quiet}) async { + int? statusCode; try { final api = "${await bind.mainGetApiServer()}/api/ab/personal"; var headers = getHttpHeaders(); headers['Content-Type'] = "application/json"; _setEmptyBody(headers); final resp = await http.post(Uri.parse(api), headers: headers); - if (resp.statusCode == 404) { + statusCode = resp.statusCode; + if (statusCode == 404) { debugPrint("HTTP 404, current api server is legacy mode"); - return false; + // Old server: keep `_personalAbGuid` null and continue in legacy mode. + return true; } Map json = _jsonDecodeRespMap(decode_http_response(resp), resp.statusCode); if (json.containsKey('error')) { throw json['error']; } - if (resp.statusCode != 200) { - throw 'HTTP ${resp.statusCode}'; + if (statusCode != 200) { + throw 'HTTP $statusCode'; } _personalAbGuid = json['guid']; + // New server: guid is available, continue in non-legacy mode. return true; } catch (err) { debugPrint('get personal ab err: ${err.toString()}'); + _setListPullError(err, quiet: quiet, statusCode: statusCode); } + // Real error: stop the current pull. return false; } - Future _getSharedAbProfiles(List profiles) async { + Future _getSharedAbProfiles(List profiles, + {required bool quiet}) async { final api = "${await bind.mainGetApiServer()}/api/ab/shared/profiles"; + int? statusCode; try { var uri0 = Uri.parse(api); final pageSize = 100; @@ -273,13 +316,19 @@ class AbModel { headers['Content-Type'] = "application/json"; _setEmptyBody(headers); final resp = await http.post(uri, headers: headers); + statusCode = resp.statusCode; + if (statusCode == 404) { + debugPrint( + "HTTP 404, api server doesn't support shared address book"); + return false; + } Map json = _jsonDecodeRespMap(decode_http_response(resp), resp.statusCode); if (json.containsKey('error')) { throw json['error']; } - if (resp.statusCode != 200) { - throw 'HTTP ${resp.statusCode}'; + if (statusCode != 200) { + throw 'HTTP $statusCode'; } if (json.containsKey('total')) { if (total == 0) total = json['total']; @@ -302,6 +351,7 @@ class AbModel { return true; } catch (err) { debugPrint('_getSharedAbProfiles err: ${err.toString()}'); + _setListPullError(err, quiet: quiet, statusCode: statusCode); } return false; } diff --git a/flutter/lib/models/group_model.dart b/flutter/lib/models/group_model.dart index c6ba992d2..d55cff453 100644 --- a/flutter/lib/models/group_model.dart +++ b/flutter/lib/models/group_model.dart @@ -343,6 +343,7 @@ class GroupModel { } reset() async { + initialized = false; groupLoadError.value = ''; deviceGroups.clear(); users.clear(); From e8a1b7fe2181025f01a361374bb8f91494c803f6 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Mon, 20 Apr 2026 10:05:32 +0800 Subject: [PATCH 219/277] fix: build (#14846) Signed-off-by: fufesou --- src/lang/ro.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 797bae8f7..7ace3f736 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -276,7 +276,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Do you accept?", "Accepți?"), ("Open System Setting", "Deschide setări sistem"), ("How to get Android input permission?", "Cum autorizez dispozitive de intrare pe Android?"), - ("android_input_permission_tip1", "Pentru ca un dispozitiv la distanță să poată controla un dispozitiv Android folosind mouse-ul sau suportul tactil, trebuie să permiți RustDesk să utilizeze serviciul „Accesibilitate"."), + ("android_input_permission_tip1", "Pentru ca un dispozitiv la distanță să poată controla un dispozitiv Android folosind mouse-ul sau suportul tactil, trebuie să permiți RustDesk să utilizeze serviciul „Accesibilitate\"."), ("android_input_permission_tip2", "Accesează următoarea pagină din Setări, deschide [Aplicații instalate] și pornește serviciul [RustDesk Input]."), ("android_new_connection_tip", "Ai primit o nouă solicitare de controlare a dispozitivului actual."), ("android_service_will_start_tip", "Activarea setării de capturare a ecranului va porni automat serviciul, permițând altor dispozitive să solicite conectarea la dispozitivul tău."), @@ -442,7 +442,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Apel vocal"), ("Text chat", "Conversație text"), ("Stop voice call", "Încheie apel vocal"), - ("relay_hint_tip", "Este posibil să nu te poți conecta direct; poți încerca să te conectezi prin retransmisie. De asemenea, dacă dorești să te conectezi direct prin retransmisie, poți adăuga sufixul „/r" la ID sau să bifezi opțiunea Conectează-te mereu prin retransmisie."), + ("relay_hint_tip", "Este posibil să nu te poți conecta direct; poți încerca să te conectezi prin retransmisie. De asemenea, dacă dorești să te conectezi direct prin retransmisie, poți adăuga sufixul „/r\" la ID sau să bifezi opțiunea Conectează-te mereu prin retransmisie."), ("Reconnect", "Reconectează-te"), ("Codec", "Codec"), ("Resolution", "Rezoluție"), From 4a50bc6fc2e123d268f60689ca7b90b201e99fd6 Mon Sep 17 00:00:00 2001 From: John Eismeier <42679190+jeis4wpi@users.noreply.github.com> Date: Tue, 21 Apr 2026 04:27:39 -0400 Subject: [PATCH 220/277] Propose fix some typos (#14857) Signed-off-by: John E --- flatpak/com.rustdesk.RustDesk.metainfo.xml | 4 +- libs/scrap/src/common/mediacodec.rs | 2 +- res/audits.py | 82 +++++++++++----------- res/msi/CustomActions/CustomActions.cpp | 10 +-- src/flutter_ffi.rs | 2 +- 5 files changed, 50 insertions(+), 50 deletions(-) mode change 100644 => 100755 res/audits.py diff --git a/flatpak/com.rustdesk.RustDesk.metainfo.xml b/flatpak/com.rustdesk.RustDesk.metainfo.xml index 0d3b33bb8..90bdafcb5 100644 --- a/flatpak/com.rustdesk.RustDesk.metainfo.xml +++ b/flatpak/com.rustdesk.RustDesk.metainfo.xml @@ -18,7 +18,7 @@
  • Supports VP8 / VP9 / AV1 software codecs, and H264 / H265 hardware codecs.
  • Own your data, easily set up self-hosting solution on your infrastructure.
  • P2P connection with end-to-end encryption based on NaCl.
  • -
  • No administrative privileges or installation needed for Windows, elevate priviledge locally or from remote on demand.
  • +
  • No administrative privileges or installation needed for Windows, elevate privilege locally or from remote on demand.
  • We like to keep things simple and will strive to make simpler where possible.
  • @@ -56,4 +56,4 @@ pointing - \ No newline at end of file + diff --git a/libs/scrap/src/common/mediacodec.rs b/libs/scrap/src/common/mediacodec.rs index bd3eace7b..8ec5e6b8f 100644 --- a/libs/scrap/src/common/mediacodec.rs +++ b/libs/scrap/src/common/mediacodec.rs @@ -151,7 +151,7 @@ fn create_media_codec(name: &str, direction: MediaCodecDirection) -> Option stop service in tray --> start service -> upgrade // Sleep(300); @@ -758,7 +758,7 @@ UINT __stdcall AddRegSoftwareSASGeneration(__in MSIHANDLE hInstall) } // Why RegSetValueExW always return 998? - // + // result = RegCreateKeyExW(HKEY_LOCAL_MACHINE, subKey, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hKey, NULL); if (result != ERROR_SUCCESS) { WcaLog(LOGMSG_STANDARD, "Failed to create or open registry key: %d", result); @@ -874,7 +874,7 @@ void TryCreateStartServiceByShell(LPWSTR svcName, LPWSTR svcBinary, LPWSTR szSvc i = 0; j = 0; // svcBinary is a string with double quotes, we need to escape it for shell arguments. - // It is orignal used for `CreateServiceW`. + // It is original used for `CreateServiceW`. // eg. "C:\Program Files\MyApp\MyApp.exe" --service -> \"C:\Program Files\MyApp\MyApp.exe\" --service while (true) { if (svcBinary[j] == L'"') { diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index e29133687..2d339f5c2 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -2884,7 +2884,7 @@ pub fn main_set_common(_key: String, _value: String) { } else if _key == "update-me" { if let Some(new_version_file) = get_download_file_from_url(&_value) { log::debug!( - "New version file is downloaed, update begin, {:?}", + "New version file is downloaded, update begin, {:?}", new_version_file.to_str() ); if let Some(f) = new_version_file.to_str() { From 803ac8cc4e51c18454bf87b0e08f83463917bd08 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Tue, 21 Apr 2026 17:34:05 +0800 Subject: [PATCH 221/277] save cargo build size --- Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 3961e9d0b..fa22dcd7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -245,3 +245,6 @@ panic = 'abort' strip = true #opt-level = 'z' # only have smaller size after strip rpath = true + +[profile.dev] +debug = 1 From 5fd20f808cbc2605051b48f830b1eb61c474807b Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Wed, 22 Apr 2026 01:29:15 +0800 Subject: [PATCH 222/277] fix safari-oidc https://github.com/rustdesk/rustdesk/issues/14861 (#14867) --- flutter/lib/web/bridge.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 1cfce661b..3d52e7d5d 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -1538,10 +1538,13 @@ class RustdeskImpl { Future mainAccountAuth( {required String op, required bool rememberMe, dynamic hint}) { - return Future(() => js.context.callMethod('setByName', [ + // Safari only allows auth popups while handling the original user gesture. + // Call into JS synchronously so the web OIDC flow can pre-open the window. + js.context.callMethod('setByName', [ 'account_auth', jsonEncode({'op': op, 'remember': rememberMe}) - ])); + ]); + return Future.value(); } Future mainAccountAuthCancel({dynamic hint}) { From b2395350090ed4a5a587afe3e1cf4b5663d73d7d Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 22 Apr 2026 01:41:13 +0800 Subject: [PATCH 223/277] refactor per code review --- flutter/lib/web/bridge.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 3d52e7d5d..a3d93f88e 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -1539,12 +1539,12 @@ class RustdeskImpl { Future mainAccountAuth( {required String op, required bool rememberMe, dynamic hint}) { // Safari only allows auth popups while handling the original user gesture. - // Call into JS synchronously so the web OIDC flow can pre-open the window. - js.context.callMethod('setByName', [ + // Use Future.sync so the JS call runs synchronously (pre-opening the OIDC + // window) while any interop error still surfaces as a Future error. + return Future.sync(() => js.context.callMethod('setByName', [ 'account_auth', jsonEncode({'op': op, 'remember': rememberMe}) - ]); - return Future.value(); + ])); } Future mainAccountAuthCancel({dynamic hint}) { From 1a41b3ac11a4e0c5399f3bc84c362b02ab59ae05 Mon Sep 17 00:00:00 2001 From: Leo Louis Date: Wed, 22 Apr 2026 15:34:09 +0530 Subject: [PATCH 224/277] Add Hindi language module and translation support (#14745) Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> --- src/lang.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lang.rs b/src/lang.rs index 85ae23c9c..682a4a51a 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -19,6 +19,7 @@ mod fa; mod gu; mod fr; mod he; +mod hi; mod hr; mod hu; mod id; @@ -96,6 +97,7 @@ pub const LANGS: &[(&str, &str)] = &[ ("ta", "தமிழ்"), ("ge", "ქართული"), ("fi", "Suomi"), + ("hi", "हिंदी"), ("gu", "ગુજરાતી"), ]; @@ -175,6 +177,7 @@ pub fn translate_locale(name: String, locale: &str) -> String { "sc" => sc::T.deref(), "ta" => ta::T.deref(), "ge" => ge::T.deref(), + "hi" => hi::T.deref(), "gu" => gu::T.deref(), _ => en::T.deref(), }; From 348d1b46e1b4fe2b9128b5e71c4224d6f9dcf792 Mon Sep 17 00:00:00 2001 From: Leo Louis Date: Wed, 22 Apr 2026 15:34:37 +0530 Subject: [PATCH 225/277] Add Hindi language support with translations (#14746) * Add Hindi language support with translations * Update print statement from 'Hello' to 'Goodbye' --- src/lang/hi.rs | 746 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 746 insertions(+) create mode 100644 src/lang/hi.rs diff --git a/src/lang/hi.rs b/src/lang/hi.rs new file mode 100644 index 000000000..d35095fd1 --- /dev/null +++ b/src/lang/hi.rs @@ -0,0 +1,746 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "स्थिति"), + ("Your Desktop", "आपका डेस्कटॉप"), + ("desk_tip", "आपका डेस्कटॉप इस आईडी और पासवर्ड से एक्सेस किया जा सकता है।"), + ("Password", "पासवर्ड"), + ("Ready", "तैयार"), + ("Established", "स्थापित"), + ("connecting_status", "नेटवर्क से जुड़ रहा है..."), + ("Enable service", "सेवा सक्षम करें"), + ("Start service", "सेवा शुरू करें"), + ("Service is running", "सेवा चल रही है"), + ("Service is not running", "सेवा नहीं चल रही है"), + ("not_ready_status", "तैयार नहीं। कृपया अपना कनेक्शन जांचें"), + ("Control Remote Desktop", "रिमोट डेस्कटॉप नियंत्रित करें"), + ("Transfer file", "फ़ाइल स्थानांतरण"), + ("Connect", "जुड़ें"), + ("Recent sessions", "हाल के सत्र"), + ("Address book", "पता पुस्तिका"), + ("Confirmation", "पुष्टि"), + ("TCP tunneling", "TCP टनलिंग"), + ("Remove", "हटाएं"), + ("Refresh random password", "यादृच्छिक (Random) पासवर्ड बदलें"), + ("Set your own password", "अपना पासवर्ड सेट करें"), + ("Enable keyboard/mouse", "कीबोर्ड/माउस सक्षम करें"), + ("Enable clipboard", "क्लिपबोर्ड सक्षम करें"), + ("Enable file transfer", "फ़ाइल स्थानांतरण सक्षम करें"), + ("Enable TCP tunneling", "TCP टनलिंग सक्षम करें"), + ("IP Whitelisting", "IP श्वेतसूची (Whitelisting)"), + ("ID/Relay Server", "ID/रिले सर्वर"), + ("Import server config", "सर्वर कॉन्फ़िगरेशन इम्पोर्ट करें"), + ("Export Server Config", "सर्वर कॉन्फ़िगरेशन एक्सपोर्ट करें"), + ("Import server configuration successfully", "सर्वर कॉन्फ़िगरेशन सफलतापूर्वक इम्पोर्ट किया गया"), + ("Export server configuration successfully", "सर्वर कॉन्फ़िगरेशन सफलतापूर्वक एक्सपोर्ट किया गया"), + ("Invalid server configuration", "अमान्य सर्वर कॉन्फ़िगरेशन"), + ("Clipboard is empty", "क्लिपबोर्ड खाली है"), + ("Stop service", "सेवा रोकें"), + ("Change ID", "ID बदलें"), + ("Your new ID", "आपकी नई ID"), + ("length %min% to %max%", "लंबाई %min% से %max% तक"), + ("starts with a letter", "एक अक्षर से शुरू होता है"), + ("allowed characters", "अनुमत अक्षर"), + ("id_change_tip", "ID बदलने के बाद वर्तमान कनेक्शन टूट जाएगा।"), + ("Website", "वेबसाइट"), + ("About", "के बारे में"), + ("Slogan_tip", "बेहतर अनुभव के लिए बनाया गया रिमोट डेस्कटॉप सॉफ़्टवेयर"), + ("Privacy Statement", "गोपनीयता कथन"), + ("Mute", "म्यूट करें"), + ("Build Date", "निर्माण तिथि"), + ("Version", "संस्करण"), + ("Home", "होम"), + ("Audio Input", "ऑडियो इनपुट"), + ("Enhancements", "वृद्धि (Enhancements)"), + ("Hardware Codec", "हार्डवेयर कोडेक"), + ("Adaptive bitrate", "अनुकूली (Adaptive) बिटरेट"), + ("ID Server", "ID सर्वर"), + ("Relay Server", "रिले सर्वर"), + ("API Server", "API सर्वर"), + ("invalid_http", "अमान्य HTTP लिंक"), + ("Invalid IP", "अमान्य IP"), + ("Invalid format", "अमान्य प्रारूप"), + ("server_not_support", "सर्वर द्वारा समर्थित नहीं"), + ("Not available", "उपलब्ध नहीं"), + ("Too frequent", "बहुत बार-बार"), + ("Cancel", "रद्द करें"), + ("Skip", "छोड़ें"), + ("Close", "बंद करें"), + ("Retry", "पुनः प्रयास करें"), + ("OK", "ठीक है"), + ("Password Required", "पासवर्ड आवश्यक है"), + ("Please enter your password", "कृपया अपना पासवर्ड दर्ज करें"), + ("Remember password", "पासवर्ड याद रखें"), + ("Wrong Password", "गलत पासवर्ड"), + ("Do you want to enter again?", "क्या आप दोबारा दर्ज करना चाहते हैं?"), + ("Connection Error", "कनेक्शन त्रुटि"), + ("Error", "त्रुटि"), + ("Reset by the peer", "दूसरे सिस्टम द्वारा रिसेट किया गया"), + ("Connecting...", "जुड़ रहा है..."), + ("Connection in progress. Please wait.", "कनेक्शन जारी है। कृपया प्रतीक्षा करें।"), + ("Please try 1 minute later", "कृपया 1 मिनट बाद पुनः प्रयास करें"), + ("Login Error", "लॉगिन त्रुटि"), + ("Successful", "सफल"), + ("Connected, waiting for image...", "जुड़ गया, इमेज की प्रतीक्षा कर रहा है..."), + ("Name", "नाम"), + ("Type", "प्रकार"), + ("Modified", "संशोधित"), + ("Size", "आकार"), + ("Show Hidden Files", "छिपी हुई फाइलें दिखाएं"), + ("Receive", "प्राप्त करें"), + ("Send", "भेजें"), + ("Refresh File", "फ़ाइल रिफ्रेश करें"), + ("Local", "स्थानीय (Local)"), + ("Remote", "रिमोट"), + ("Remote Computer", "रिमोट कंप्यूटर"), + ("Local Computer", "स्थानीय कंप्यूटर"), + ("Confirm Delete", "हटाने की पुष्टि करें"), + ("Delete", "हटाएं"), + ("Properties", "गुण (Properties)"), + ("Multi Select", "बहु-चयन"), + ("Select All", "सभी चुनें"), + ("Unselect All", "सभी अचयनित करें"), + ("Empty Directory", "खाली निर्देशिका"), + ("Not an empty directory", "निर्देशिका खाली नहीं है"), + ("Are you sure you want to delete this file?", "क्या आप वाकई इस फ़ाइल को हटाना चाहते हैं?"), + ("Are you sure you want to delete this empty directory?", "क्या आप वाकई इस खाली निर्देशिका को हटाना चाहते हैं?"), + ("Are you sure you want to delete the file of this directory?", "क्या आप वाकई इस निर्देशिका की फ़ाइल को हटाना चाहते हैं?"), + ("Do this for all conflicts", "सभी विवादों के लिए यह करें"), + ("This is irreversible!", "इसे वापस नहीं लिया जा सकता!"), + ("Deleting", "हटाया जा रहा है"), + ("files", "फाइलें"), + ("Waiting", "प्रतीक्षा कर रहा है"), + ("Finished", "पूरा हुआ"), + ("Speed", "गति"), + ("Custom Image Quality", "कस्टम इमेज गुणवत्ता"), + ("Privacy mode", "गोपनीयता मोड"), + ("Block user input", "उपयोगकर्ता इनपुट ब्लॉक करें"), + ("Unblock user input", "उपयोगकर्ता इनपुट अनब्लॉक करें"), + ("Adjust Window", "विंडो समायोजित करें"), + ("Original", "मूल (Original)"), + ("Shrink", "सिकुड़ें"), + ("Stretch", "खिंचाव (Stretch)"), + ("Scrollbar", "स्क्रोलबार"), + ("ScrollAuto", "ऑटो स्क्रॉल"), + ("Good image quality", "अच्छी इमेज गुणवत्ता"), + ("Balanced", "संतुलित"), + ("Optimize reaction time", "प्रतिक्रिया समय अनुकूलित करें"), + ("Custom", "कस्टम"), + ("Show remote cursor", "रिमोट कर्सर दिखाएं"), + ("Show quality monitor", "गुणवत्ता मॉनिटर दिखाएं"), + ("Disable clipboard", "क्लिपबोर्ड अक्षम करें"), + ("Lock after session end", "सत्र समाप्त होने के बाद लॉक करें"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del डालें"), + ("Insert Lock", "लॉक डालें"), + ("Refresh", "रिफ्रेश करें"), + ("ID does not exist", "ID मौजूद नहीं है"), + ("Failed to connect to rendezvous server", "Rendezvous सर्वर से जुड़ने में विफल"), + ("Please try later", "कृपया बाद में प्रयास करें"), + ("Remote desktop is offline", "रिमोट डेस्कटॉप ऑफ़लाइन है"), + ("Key mismatch", "कुंजी बेमेल (Key mismatch)"), + ("Timeout", "समय समाप्त"), + ("Failed to connect to relay server", "रिले सर्वर से जुड़ने में विफल"), + ("Failed to connect via rendezvous server", "Rendezvous सर्वर के माध्यम से जुड़ने में विफल"), + ("Failed to connect via relay server", "रिले सर्वर के माध्यम से जुड़ने में विफल"), + ("Failed to make direct connection to remote desktop", "रिमोट डेस्कटॉप से सीधा कनेक्शन बनाने में विफल"), + ("Set Password", "पासवर्ड सेट करें"), + ("OS Password", "OS पासवर्ड"), + ("install_tip", "सर्वोत्तम प्रदर्शन के लिए, इसे इंस्टॉल करें।"), + ("Click to upgrade", "अपग्रेड करने के लिए क्लिक करें"), + ("Configure", "कॉन्फ़िगर करें"), + ("config_acc", "एक्सेसिबिलिटी कॉन्फ़िगर करें"), + ("config_screen", "स्क्रीन कॉन्फ़िगर करें"), + ("Installing ...", "इंस्टॉल हो रहा है..."), + ("Install", "इंस्टॉल करें"), + ("Installation", "इंस्टॉलेशन"), + ("Installation Path", "इंस्टॉलेशन पाथ"), + ("Create start menu shortcuts", "स्टार्ट मेनू शॉर्टकट बनाएं"), + ("Create desktop icon", "डेस्कटॉप आइकन बनाएं"), + ("agreement_tip", "इंस्टॉल करके आप लाइसेंस समझौते को स्वीकार करते हैं।"), + ("Accept and Install", "स्वीकार करें और इंस्टॉल करें"), + ("End-user license agreement", "अंतिम उपयोगकर्ता लाइसेंस समझौता"), + ("Generating ...", "बनाया जा रहा है..."), + ("Your installation is lower version.", "आपका वर्तमान इंस्टॉलेशन पुराना संस्करण है।"), + ("not_close_tcp_tip", "टनल का उपयोग करते समय इस विंडो को बंद न करें।"), + ("Listening ...", "सुन रहा है (Listening)..."), + ("Remote Host", "रिमोट होस्ट"), + ("Remote Port", "रिमोट पोर्ट"), + ("Action", "कार्य"), + ("Add", "जोड़ें"), + ("Local Port", "स्थानीय पोर्ट"), + ("Local Address", "स्थानीय पता"), + ("Change Local Port", "स्थानीय पोर्ट बदलें"), + ("setup_server_tip", "तेज़ कनेक्शन के लिए अपना खुद का सर्वर सेटअप करें"), + ("Too short, at least 6 characters.", "बहुत छोटा, कम से कम 6 अक्षर होने चाहिए।"), + ("The confirmation is not identical.", "पुष्टि समान नहीं है।"), + ("Permissions", "अनुमतियाँ"), + ("Accept", "स्वीकार करें"), + ("Dismiss", "खारिज करें"), + ("Disconnect", "डिस्कनेक्ट करें"), + ("Enable file copy and paste", "फ़ाइल कॉपी और पेस्ट सक्षम करें"), + ("Connected", "जुड़ गया"), + ("Direct and encrypted connection", "सीधा और एन्क्रिप्टेड कनेक्शन"), + ("Relayed and encrypted connection", "रिले और एन्क्रिप्टेड कनेक्शन"), + ("Direct and unencrypted connection", "सीधा और अनएन्क्रिप्टेड कनेक्शन"), + ("Relayed and unencrypted connection", "रिले और अनएन्क्रिप्टेड कनेक्शन"), + ("Enter Remote ID", "रिमोट ID दर्ज करें"), + ("Enter your password", "अपना पासवर्ड दर्ज करें"), + ("Logging in...", "लॉग इन हो रहा है..."), + ("Enable RDP session sharing", "RDP सत्र साझाकरण सक्षम करें"), + ("Auto Login", "ऑटो लॉगिन"), + ("Enable direct IP access", "सीधी IP पहुंच सक्षम करें"), + ("Rename", "नाम बदलें"), + ("Space", "स्थान (Space)"), + ("Create desktop shortcut", "डेस्कटॉप शॉर्टकट बनाएं"), + ("Change Path", "पाथ बदलें"), + ("Create Folder", "फ़ोल्डर बनाएं"), + ("Please enter the folder name", "कृपया फ़ोल्डर का नाम दर्ज करें"), + ("Fix it", "इसे ठीक करें"), + ("Warning", "चेतावनी"), + ("Login screen using Wayland is not supported", "Wayland का उपयोग करने वाली लॉगिन स्क्रीन समर्थित नहीं है"), + ("Reboot required", "रीबूट आवश्यक है"), + ("Unsupported display server", "असमर्थित डिस्प्ले सर्वर"), + ("x11 expected", "x11 अपेक्षित है"), + ("Port", "पोर्ट"), + ("Settings", "सेटिंग्स"), + ("Username", "उपयोगकर्ता नाम"), + ("Invalid port", "अमान्य पोर्ट"), + ("Closed manually by the peer", "दूसरे सिस्टम द्वारा मैन्युअल रूप से बंद किया गया"), + ("Enable remote configuration modification", "रिमोट कॉन्फ़िगरेशन संशोधन सक्षम करें"), + ("Run without install", "बिना इंस्टॉल किए चलाएं"), + ("Connect via relay", "रिले के माध्यम से जुड़ें"), + ("Always connect via relay", "हमेशा रिले के माध्यम से जुड़ें"), + ("whitelist_tip", "केवल श्वेतसूचीबद्ध IP ही मुझ तक पहुंच सकते हैं"), + ("Login", "लॉगिन"), + ("Verify", "सत्यापित करें"), + ("Remember me", "मुझे याद रखें"), + ("Trust this device", "इस डिवाइस पर भरोसा करें"), + ("Verification code", "सत्यापन कोड"), + ("verification_tip", "एक सत्यापन कोड आपके ईमेल पर भेजा गया है"), + ("Logout", "लॉगआउट"), + ("Tags", "टैग"), + ("Search ID", "ID खोजें"), + ("whitelist_sep", "अल्पविराम, अर्धविराम या रिक्त स्थान द्वारा अलग किया गया"), + ("Add ID", "ID जोड़ें"), + ("Add Tag", "टैग जोड़ें"), + ("Unselect all tags", "सभी टैग अचयनित करें"), + ("Network error", "नेटवर्क त्रुटि"), + ("Username missed", "उपयोगकर्ता नाम छूट गया"), + ("Password missed", "पासवर्ड छूट गया"), + ("Wrong credentials", "गलत क्रेडेंशियल"), + ("The verification code is incorrect or has expired", "सत्यापन कोड गलत है या समाप्त हो गया है"), + ("Edit Tag", "टैग संपादित करें"), + ("Forget Password", "पासवर्ड भूल गए"), + ("Favorites", "पसंदीदा"), + ("Add to Favorites", "पसंदीदा में जोड़ें"), + ("Remove from Favorites", "पसंदीदा से हटाएं"), + ("Empty", "खाली"), + ("Invalid folder name", "अमान्य फ़ोल्डर नाम"), + ("Socks5 Proxy", "Socks5 प्रॉक्सी"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) प्रॉक्सी"), + ("Discovered", "खोजा गया"), + ("install_daemon_tip", "बूट पर शुरू करने के लिए सेवा इंस्टॉल करें"), + ("Remote ID", "रिमोट ID"), + ("Paste", "पेस्ट करें"), + ("Paste here?", "यहाँ पेस्ट करें?"), + ("Are you sure to close the connection?", "क्या आप वाकई कनेक्शन बंद करना चाहते हैं?"), + ("Download new version", "नया संस्करण डाउनलोड करें"), + ("Touch mode", "टच मोड"), + ("Mouse mode", "माउस मोड"), + ("One-Finger Tap", "एक उंगली से टैप"), + ("Left Mouse", "बायां माउस"), + ("One-Long Tap", "एक लंबा टैप"), + ("Two-Finger Tap", "दो उंगलियों से टैप"), + ("Right Mouse", "दायां माउस"), + ("One-Finger Move", "एक उंगली से हिलाएं"), + ("Double Tap & Move", "डबल टैप और हिलाएं"), + ("Mouse Drag", "माउस ड्रैग"), + ("Three-Finger vertically", "तीन उंगलियां लंबवत"), + ("Mouse Wheel", "माउस व्हील"), + ("Two-Finger Move", "दो उंगलियों से हिलाएं"), + ("Canvas Move", "कैनवास मूव"), + ("Pinch to Zoom", "ज़ूम करने के लिए पिंच करें"), + ("Canvas Zoom", "कैनवास ज़ूम"), + ("Reset canvas", "कैनवास रिसेट करें"), + ("No permission of file transfer", "फ़ाइल स्थानांतरण की अनुमति नहीं है"), + ("Note", "नोट"), + ("Connection", "कनेक्शन"), + ("Share screen", "स्क्रीन शेयर करें"), + ("Chat", "चैट"), + ("Total", "कुल"), + ("items", "आइटम"), + ("Selected", "चयनित"), + ("Screen Capture", "स्क्रीन कैप्चर"), + ("Input Control", "इनपुट नियंत्रण"), + ("Audio Capture", "ऑडियो कैप्चर"), + ("Do you accept?", "क्या आप स्वीकार करते हैं?"), + ("Open System Setting", "सिस्टम सेटिंग खोलें"), + ("How to get Android input permission?", "Android इनपुट अनुमति कैसे प्राप्त करें?"), + ("android_input_permission_tip1", "इनपुट अनुमति प्राप्त करने के लिए एक्सेसिबिलिटी सेवा सक्षम करें।"), + ("android_input_permission_tip2", "कृपया सिस्टम सेटिंग में RustDesk खोजें और इसे चालू करें।"), + ("android_new_connection_tip", "एक नया नियंत्रण अनुरोध प्राप्त हुआ है।"), + ("android_service_will_start_tip", "स्क्रीन कैप्चर चालू करने से सेवा अपने आप शुरू हो जाएगी।"), + ("android_stop_service_tip", "सेवा बंद करने से सभी कनेक्शन टूट जाएंगे।"), + ("android_version_audio_tip", "ऑडियो कैप्चर केवल Android 10 या उच्चतर पर समर्थित है।"), + ("android_start_service_tip", "स्क्रीन शेयरिंग सेवा शुरू करने के लिए क्लिक करें।"), + ("android_permission_may_not_change_tip", "अनुमतियाँ बाद में नहीं बदली जा सकती हैं, कृपया ध्यान से चुनें।"), + ("Account", "खाता"), + ("Overwrite", "ओवरराइट (Overwrite) करें"), + ("This file exists, skip or overwrite this file?", "यह फ़ाइल मौजूद है, छोड़ें या ओवरराइट करें?"), + ("Quit", "बाहर निकलें"), + ("Help", "सहायता"), + ("Failed", "विफल"), + ("Succeeded", "सफल"), + ("Someone turns on privacy mode, exit", "किसी ने गोपनीयता मोड चालू किया है, बाहर निकल रहे हैं"), + ("Unsupported", "असमर्थित"), + ("Peer denied", "दूसरे सिस्टम ने मना कर दिया"), + ("Please install plugins", "कृपया प्लगइन्स इंस्टॉल करें"), + ("Peer exit", "दूसरा सिस्टम बाहर निकल गया"), + ("Failed to turn off", "बंद करने में विफल"), + ("Turned off", "बंद कर दिया गया"), + ("Language", "भाषा"), + ("Keep RustDesk background service", "RustDesk बैकग्राउंड सेवा चालू रखें"), + ("Ignore Battery Optimizations", "बैटरी ऑप्टिमाइजेशन को अनदेखा करें"), + ("android_open_battery_optimizations_tip", "डिस्कनेक्शन से बचने के लिए बैटरी ऑप्टिमाइजेशन सेटिंग खोलें"), + ("Start on boot", "बूट पर शुरू करें"), + ("Start the screen sharing service on boot, requires special permissions", "बूट पर स्क्रीन शेयरिंग सेवा शुरू करें, विशेष अनुमतियों की आवश्यकता है"), + ("Connection not allowed", "कनेक्शन की अनुमति नहीं है"), + ("Legacy mode", "लेगेसी (Legacy) मोड"), + ("Map mode", "मैप मोड"), + ("Translate mode", "अनुवाद मोड"), + ("Use permanent password", "स्थायी पासवर्ड का उपयोग करें"), + ("Use both passwords", "दोनों पासवर्ड का उपयोग करें"), + ("Set permanent password", "स्थायी पासवर्ड सेट करें"), + ("Enable remote restart", "रिमोट रीस्टार्ट सक्षम करें"), + ("Restart remote device", "रिमोट डिवाइस रीस्टार्ट करें"), + ("Are you sure you want to restart", "क्या आप वाकई रीस्टार्ट करना चाहते हैं?"), + ("Restarting remote device", "रिमोट डिवाइस रीस्टार्ट हो रहा है"), + ("remote_restarting_tip", "रिमोट डिवाइस रीस्टार्ट हो रहा है, कृपया प्रतीक्षा करें..."), + ("Copied", "कॉपी किया गया"), + ("Exit Fullscreen", "फुलस्क्रीन से बाहर निकलें"), + ("Fullscreen", "फुलस्क्रीन"), + ("Mobile Actions", "मोबाइल क्रियाएं"), + ("Select Monitor", "मॉनिटर चुनें"), + ("Control Actions", "नियंत्रण क्रियाएं"), + ("Display Settings", "डिस्प्ले सेटिंग्स"), + ("Ratio", "अनुपात (Ratio)"), + ("Image Quality", "इमेज गुणवत्ता"), + ("Scroll Style", "स्क्रॉल शैली"), + ("Show Toolbar", "टूलबार दिखाएं"), + ("Hide Toolbar", "टूलबार छुपाएं"), + ("Direct Connection", "सीधा कनेक्शन"), + ("Relay Connection", "रिले कनेक्शन"), + ("Secure Connection", "सुरक्षित कनेक्शन"), + ("Insecure Connection", "असुरक्षित कनेक्शन"), + ("Scale original", "मूल पैमाना"), + ("Scale adaptive", "अनुकूली पैमाना"), + ("General", "सामान्य"), + ("Security", "सुरक्षा"), + ("Theme", "थीम"), + ("Dark Theme", "डार्क थीम"), + ("Light Theme", "लाइट थीम"), + ("Dark", "डार्क"), + ("Light", "लाइट"), + ("Follow System", "सिस्टम का पालन करें"), + ("Enable hardware codec", "हार्डवेयर कोडेक सक्षम करें"), + ("Unlock Security Settings", "सुरक्षा सेटिंग्स अनलॉक करें"), + ("Enable audio", "ऑडियो सक्षम करें"), + ("Unlock Network Settings", "नेटवर्क सेटिंग्स अनलॉक करें"), + ("Server", "सर्वर"), + ("Direct IP Access", "सीधी IP पहुंच"), + ("Proxy", "प्रॉक्सी"), + ("Apply", "लागू करें"), + ("Disconnect all devices?", "सभी डिवाइस डिस्कनेक्ट करें?"), + ("Clear", "साफ करें"), + ("Audio Input Device", "ऑडियो इनपुट डिवाइस"), + ("Use IP Whitelisting", "IP श्वेतसूची का उपयोग करें"), + ("Network", "नेटवर्क"), + ("Pin Toolbar", "टूलबार पिन करें"), + ("Unpin Toolbar", "टूलबार अनपिन करें"), + ("Recording", "रिकॉर्डिंग"), + ("Directory", "निर्देशिका"), + ("Automatically record incoming sessions", "आने वाले सत्रों को स्वचालित रूप से रिकॉर्ड करें"), + ("Automatically record outgoing sessions", "जाने वाले सत्रों को स्वचालित रूप से रिकॉर्ड करें"), + ("Change", "बदलें"), + ("Start session recording", "सत्र रिकॉर्डिंग शुरू करें"), + ("Stop session recording", "सत्र रिकॉर्डिंग रोकें"), + ("Enable recording session", "सत्र रिकॉर्डिंग सक्षम करें"), + ("Enable LAN discovery", "LAN खोज सक्षम करें"), + ("Deny LAN discovery", "LAN खोज अस्वीकार करें"), + ("Write a message", "संदेश लिखें"), + ("Prompt", "प्रॉम्प्ट"), + ("Please wait for confirmation of UAC...", "कृपया UAC की पुष्टि की प्रतीक्षा करें..."), + ("elevated_foreground_window_tip", "रिमोट डेस्कटॉप की वर्तमान विंडो को उच्च अनुमतियों की आवश्यकता है।"), + ("Disconnected", "डिस्कनेक्ट हो गया"), + ("Other", "अन्य"), + ("Confirm before closing multiple tabs", "एकाधिक टैब बंद करने से पहले पुष्टि करें"), + ("Keyboard Settings", "कीबोर्ड सेटिंग्स"), + ("Full Access", "पूर्ण पहुंच (Full Access)"), + ("Screen Share", "स्क्रीन शेयर"), + ("ubuntu-21-04-required", "Ubuntu 21.04 या उच्चतर आवश्यक है"), + ("wayland-requires-higher-linux-version", "Wayland के लिए उच्च Linux संस्करण आवश्यक है"), + ("xdp-portal-unavailable", "XDP पोर्टल अनुपलब्ध है"), + ("JumpLink", "JumpLink"), + ("Please Select the screen to be shared(Operate on the peer side).", "कृपया साझा की जाने वाली स्क्रीन चुनें (दूसरे सिस्टम पर संचालित करें)।"), + ("Show RustDesk", "RustDesk दिखाएं"), + ("This PC", "यह PC"), + ("or", "या"), + ("Elevate", "एलीवेट (Elevate) करें"), + ("Zoom cursor", "ज़ूम कर्सर"), + ("Accept sessions via password", "पासवर्ड के माध्यम से सत्र स्वीकार करें"), + ("Accept sessions via click", "क्लिक के माध्यम से सत्र स्वीकार करें"), + ("Accept sessions via both", "दोनों के माध्यम से सत्र स्वीकार करें"), + ("Please wait for the remote side to accept your session request...", "कृपया रिमोट साइड द्वारा आपके सत्र अनुरोध को स्वीकार करने की प्रतीक्षा करें..."), + ("One-time Password", "वन-टाइम पासवर्ड"), + ("Use one-time password", "वन-टाइम पासवर्ड का उपयोग करें"), + ("One-time password length", "वन-टाइम पासवर्ड की लंबाई"), + ("Request access to your device", "आपके डिवाइस तक पहुंच का अनुरोध"), + ("Hide connection management window", "कनेक्शन प्रबंधन विंडो छुपाएं"), + ("hide_cm_tip", "केवल तभी छुपाएं जब पासवर्ड से कनेक्शन की अनुमति हो"), + ("wayland_experiment_tip", "Wayland समर्थन अभी परीक्षण मोड में है"), + ("Right click to select tabs", "टैब चुनने के लिए राइट क्लिक करें"), + ("Skipped", "छोड़ दिया गया"), + ("Add to address book", "पता पुस्तिका में जोड़ें"), + ("Group", "समूह"), + ("Search", "खोजें"), + ("Closed manually by web console", "वेब कंसोल द्वारा मैन्युअल रूप से बंद किया गया"), + ("Local keyboard type", "स्थानीय कीबोर्ड प्रकार"), + ("Select local keyboard type", "स्थानीय कीबोर्ड प्रकार चुनें"), + ("software_render_tip", "यदि आपकी स्क्रीन काली है, तो इसे आज़माएं"), + ("Always use software rendering", "हमेशा सॉफ़्टवेयर रेंडरिंग का उपयोग करें"), + ("config_input", "इनपुट कॉन्फ़िगर करें"), + ("config_microphone", "माइक्रोफ़ोन कॉन्फ़िगर करें"), + ("request_elevation_tip", "रिमोट साइड से उच्च अनुमतियों का अनुरोध करें"), + ("Wait", "प्रतीक्षा करें"), + ("Elevation Error", "एलीवेशन (Elevation) त्रुटि"), + ("Ask the remote user for authentication", "रिमोट उपयोगकर्ता से प्रमाणीकरण मांगें"), + ("Choose this if the remote account is administrator", "यदि रिमोट खाता व्यवस्थापक (Admin) है तो इसे चुनें"), + ("Transmit the username and password of administrator", "व्यवस्थापक का उपयोगकर्ता नाम और पासवर्ड भेजें"), + ("still_click_uac_tip", "रिमोट उपयोगकर्ता को अभी भी UAC विंडो पर 'हाँ' क्लिक करना होगा।"), + ("Request Elevation", "एलीवेशन का अनुरोध करें"), + ("wait_accept_uac_tip", "कृपया रिमोट उपयोगकर्ता द्वारा UAC स्वीकार करने की प्रतीक्षा करें।"), + ("Elevate successfully", "सफलतापूर्वक एलीवेट किया गया"), + ("uppercase", "बड़े अक्षर (Uppercase)"), + ("lowercase", "छोटे अक्षर (Lowercase)"), + ("digit", "अंक (Digit)"), + ("special character", "विशेष वर्ण"), + ("length>=8", "लंबाई >= 8"), + ("Weak", "कमजोर"), + ("Medium", "मध्यम"), + ("Strong", "मजबूत"), + ("Switch Sides", "साइड्स बदलें"), + ("Please confirm if you want to share your desktop?", "कृपया पुष्टि करें कि क्या आप अपना डेस्कटॉप साझा करना चाहते हैं?"), + ("Display", "डिस्प्ले"), + ("Default View Style", "डिफ़ॉल्ट व्यू शैली"), + ("Default Scroll Style", "डिफ़ॉल्ट स्क्रॉल शैली"), + ("Default Image Quality", "डिफ़ॉल्ट इमेज गुणवत्ता"), + ("Default Codec", "डिफ़ॉल्ट कोडेक"), + ("Bitrate", "बिटरेट"), + ("FPS", "FPS"), + ("Auto", "ऑटो"), + ("Other Default Options", "अन्य डिफ़ॉल्ट विकल्प"), + ("Voice call", "वॉयस कॉल"), + ("Text chat", "टेक्स्ट चैट"), + ("Stop voice call", "वॉयस कॉल बंद करें"), + ("relay_hint_tip", "सीधा कनेक्शन संभव नहीं हो सकता; आप रिले के माध्यम से जुड़ने का प्रयास कर सकते हैं।"), + ("Reconnect", "पुनः कनेक्ट करें"), + ("Codec", "कोडेक"), + ("Resolution", "रिज़ॉल्यूशन"), + ("No transfers in progress", "कोई स्थानांतरण जारी नहीं है"), + ("Set one-time password length", "वन-टाइम पासवर्ड की लंबाई सेट करें"), + ("RDP Settings", "RDP सेटिंग्स"), + ("Sort by", "इसके अनुसार क्रमबद्ध करें"), + ("New Connection", "नया कनेक्शन"), + ("Restore", "पुनर्स्थापित करें"), + ("Minimize", "मिनिमाइज करें"), + ("Maximize", "मैक्सिमाइज करें"), + ("Your Device", "आपका डिवाइस"), + ("empty_recent_tip", "हाल के सत्र यहाँ दिखाई देंगे।"), + ("empty_favorite_tip", "पसंदीदा डिवाइस यहाँ दिखाई देंगे।"), + ("empty_lan_tip", "खोजे गए डिवाइस यहाँ दिखाई देंगे।"), + ("empty_address_book_tip", "आपके पता पुस्तिका में वर्तमान में कोई डिवाइस नहीं है।"), + ("Empty Username", "खाली उपयोगकर्ता नाम"), + ("Empty Password", "खाली पासवर्ड"), + ("Me", "मैं"), + ("identical_file_tip", "यह फ़ाइल पहले से ही मौजूद है।"), + ("show_monitors_tip", "टूलबार में मॉनिटर दिखाएं"), + ("View Mode", "व्यू मोड"), + ("login_linux_tip", "रिमोट Linux सत्र शुरू करने के लिए आपको लॉगिन करना होगा"), + ("verify_rustdesk_password_tip", "RustDesk पासवर्ड सत्यापित करें"), + ("remember_account_tip", "इस खाते को याद रखें"), + ("os_account_desk_tip", "रिमोट डेस्कटॉप को एक्सेस करने के लिए OS खाते का उपयोग करें"), + ("OS Account", "OS खाता"), + ("another_user_login_title_tip", "एक अन्य उपयोगकर्ता पहले से ही लॉगिन है"), + ("another_user_login_text_tip", "डिस्कनेक्ट करें और पुनः प्रयास करें"), + ("xorg_not_found_title_tip", "Xorg नहीं मिला"), + ("xorg_not_found_text_tip", "कृपया Xorg इंस्टॉल करें"), + ("no_desktop_title_tip", "कोई डेस्कटॉप उपलब्ध नहीं है"), + ("no_desktop_text_tip", "कृपया Linux डेस्कटॉप इंस्टॉल करें"), + ("No need to elevate", "एलीवेट करने की आवश्यकता नहीं है"), + ("System Sound", "सिस्टम साउंड"), + ("Default", "डिफ़ॉल्ट"), + ("New RDP", "नया RDP"), + ("Fingerprint", "फिंगरप्रिंट"), + ("Copy Fingerprint", "फिंगरप्रिंट कॉपी करें"), + ("no fingerprints", "कोई फिंगरप्रिंट नहीं"), + ("Select a peer", "एक पीयर (Peer) चुनें"), + ("Select peers", "पीयर्स चुनें"), + ("Plugins", "प्लगइन्स"), + ("Uninstall", "अनइंस्टॉल करें"), + ("Update", "अपडेट करें"), + ("Enable", "सक्षम करें"), + ("Disable", "अक्षम करें"), + ("Options", "विकल्प"), + ("resolution_original_tip", "मूल रिज़ॉल्यूशन"), + ("resolution_fit_local_tip", "स्थानीय स्क्रीन में फिट करें"), + ("resolution_custom_tip", "कस्टम रिज़ॉल्यूशन"), + ("Collapse toolbar", "टूलबार समेटें"), + ("Accept and Elevate", "स्वीकार करें और एलीवेट करें"), + ("accept_and_elevate_btn_tooltip", "कनेक्शन स्वीकार करें और UAC अनुमतियाँ मांगें।"), + ("clipboard_wait_response_timeout_tip", "क्लिपबोर्ड प्रतिक्रिया के लिए समय समाप्त हो गया।"), + ("Incoming connection", "आने वाला कनेक्शन"), + ("Outgoing connection", "जाने वाला कनेक्शन"), + ("Exit", "बाहर निकलें"), + ("Open", "खोलें"), + ("logout_tip", "क्या आप वाकई लॉगआउट करना चाहते हैं?"), + ("Service", "सेवा"), + ("Start", "शुरू करें"), + ("Stop", "रोकें"), + ("exceed_max_devices", "आप डिवाइस की अधिकतम सीमा को पार कर चुके हैं।"), + ("Sync with recent sessions", "हाल के सत्रों के साथ सिंक करें"), + ("Sort tags", "टैग क्रमबद्ध करें"), + ("Open connection in new tab", "नये टैब में कनेक्शन खोलें"), + ("Move tab to new window", "टैब को नयी विंडो में ले जाएं"), + ("Can not be empty", "खाली नहीं हो सकता"), + ("Already exists", "पहले से मौजूद है"), + ("Change Password", "पासवर्ड बदलें"), + ("Refresh Password", "पासवर्ड रिफ्रेश करें"), + ("ID", "ID"), + ("Grid View", "ग्रिड व्यू"), + ("List View", "लिस्ट व्यू"), + ("Select", "चुनें"), + ("Toggle Tags", "टैग टॉगल करें"), + ("pull_ab_failed_tip", "पता पुस्तिका अपडेट करने में विफल।"), + ("push_ab_failed_tip", "सर्वर पर पता पुस्तिका सिंक करने में विफल।"), + ("synced_peer_readded_tip", "हाल के सत्रों में मौजूद डिवाइस पता पुस्तिका में सिंक किए गए थे।"), + ("Change Color", "रंग बदलें"), + ("Primary Color", "प्राथमिक रंग"), + ("HSV Color", "HSV रंग"), + ("Installation Successful!", "इंस्टॉलेशन सफल रहा!"), + ("Installation failed!", "इंस्टॉलेशन विफल रहा!"), + ("Reverse mouse wheel", "माउस व्हील उल्टा करें"), + ("{} sessions", "{} सत्र"), + ("scam_title", "धोखाधड़ी की चेतावनी!"), + ("scam_text1", "यदि आप किसी ऐसे व्यक्ति से बात कर रहे हैं जिसे आप नहीं जानते और जिसने आपसे RustDesk उपयोग करने को कहा है, तो तुरंत डिस्कनेक्ट कर दें।"), + ("scam_text2", "यह एक घोटाला हो सकता है। अपना आईडी या पासवर्ड किसी को न दें।"), + ("Don't show again", "दोबारा न दिखाएं"), + ("I Agree", "मैं सहमत हूँ"), + ("Decline", "अस्वीकार करें"), + ("Timeout in minutes", "मिनटों में टाइमआउट"), + ("auto_disconnect_option_tip", "निष्क्रियता पर स्वचालित रूप से डिस्कनेक्ट करें"), + ("Connection failed due to inactivity", "निष्क्रियता के कारण कनेक्शन विफल रहा"), + ("Check for software update on startup", "स्टार्टअप पर सॉफ़्टवेयर अपडेट की जांच करें"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "RustDesk सर्वर प्रो को संस्करण {} में अपग्रेड करें"), + ("pull_group_failed_tip", "समूह खींचने (Pull) में विफल"), + ("Filter by intersection", "इंटरसेक्शन द्वारा फ़िल्टर करें"), + ("Remove wallpaper during incoming sessions", "आने वाले सत्रों के दौरान वॉलपेपर हटा दें"), + ("Test", "परीक्षण"), + ("display_is_plugged_out_msg", "डिस्प्ले हटा दिया गया है।"), + ("No displays", "कोई डिस्प्ले नहीं"), + ("Open in new window", "नयी विंडो में खोलें"), + ("Show displays as individual windows", "डिस्प्ले को व्यक्तिगत विंडो के रूप में दिखाएं"), + ("Use all my displays for the remote session", "रिमोट सत्र के लिए मेरे सभी डिस्प्ले का उपयोग करें"), + ("selinux_tip", "डिवाइस पर SELinux सक्षम है।"), + ("Change view", "व्यू बदलें"), + ("Big tiles", "बड़ी टाइलें"), + ("Small tiles", "छोटी टाइलें"), + ("List", "लिस्ट"), + ("Virtual display", "वर्चुअल डिस्प्ले"), + ("Plug out all", "सभी अनप्लग करें"), + ("True color (4:4:4)", "सच्चा रंग (4:4:4)"), + ("Enable blocking user input", "उपयोगकर्ता इनपुट को ब्लॉक करना सक्षम करें"), + ("id_input_tip", "आप ID, उपनाम (Alias) या IP पता दर्ज कर सकते हैं।"), + ("privacy_mode_impl_mag_tip", "मैग्निफायर (Magnifier) गोपनीयता मोड"), + ("privacy_mode_impl_virtual_display_tip", "वर्चुअल डिस्प्ले गोपनीयता मोड"), + ("Enter privacy mode", "गोपनीयता मोड में प्रवेश करें"), + ("Exit privacy mode", "गोपनीयता मोड से बाहर निकलें"), + ("idd_not_support_under_win10_2004_tip", "वर्चुअल डिस्प्ले Windows 10 संस्करण 2004 या उच्चतर पर समर्थित है।"), + ("input_source_1_tip", "इनपुट स्रोत 1"), + ("input_source_2_tip", "इनपुट स्रोत 2"), + ("Swap control-command key", "Control और Command कुंजियों को बदलें"), + ("swap-left-right-mouse", "बाएं और दाएं माउस बटन को बदलें"), + ("2FA code", "2FA कोड"), + ("More", "अधिक"), + ("enable-2fa-title", "द्वि-कारक प्रमाणीकरण (2FA) सक्षम करें"), + ("enable-2fa-desc", "कृपया अपना ऑथेंटिकेटर ऐप सेट करें।"), + ("wrong-2fa-code", "गलत 2FA कोड।"), + ("enter-2fa-title", "2FA कोड दर्ज करें"), + ("Email verification code must be 6 characters.", "ईमेल सत्यापन कोड 6 अक्षरों का होना चाहिए।"), + ("2FA code must be 6 digits.", "2FA कोड 6 अंकों का होना चाहिए।"), + ("Multiple Windows sessions found", "एकाधिक Windows सत्र मिले"), + ("Please select the session you want to connect to", "कृपया वह सत्र चुनें जिससे आप जुड़ना चाहते हैं"), + ("powered_by_me", "मेरे द्वारा संचालित"), + ("outgoing_only_desk_tip", "यह केवल आउटगोइंग मोड है"), + ("preset_password_warning", "सुरक्षा के लिए, कृपया डिफ़ॉल्ट पासवर्ड बदलें।"), + ("Security Alert", "सुरक्षा चेतावनी"), + ("My address book", "मेरी पता पुस्तिका"), + ("Personal", "व्यक्तिगत"), + ("Owner", "स्वामी"), + ("Set shared password", "साझा पासवर्ड सेट करें"), + ("Exist in", "इसमें मौजूद है"), + ("Read-only", "केवल पढ़ने के लिए"), + ("Read/Write", "पढ़ना/लिखना"), + ("Full Control", "पूर्ण नियंत्रण"), + ("share_warning_tip", "सावधानी: आप अपना एक्सेस साझा कर रहे हैं।"), + ("Everyone", "हर कोई"), + ("ab_web_console_tip", "वेब कंसोल पता पुस्तिका"), + ("allow-only-conn-window-open-tip", "केवल तभी कनेक्शन की अनुमति दें जब RustDesk विंडो खुली हो"), + ("no_need_privacy_mode_no_physical_displays_tip", "कोई भौतिक डिस्प्ले नहीं मिला, गोपनीयता मोड की आवश्यकता नहीं है।"), + ("Follow remote cursor", "रिमोट कर्सर का पालन करें"), + ("Follow remote window focus", "रिमोट विंडो फोकस का पालन करें"), + ("default_proxy_tip", "डिफ़ॉल्ट प्रॉक्सी सेटिंग"), + ("no_audio_input_device_tip", "कोई ऑडियो इनपुट डिवाइस नहीं मिला।"), + ("Incoming", "आने वाली"), + ("Outgoing", "जाने वाली"), + ("Clear Wayland screen selection", "Wayland स्क्रीन चयन साफ़ करें"), + ("clear_Wayland_screen_selection_tip", "Wayland के स्क्रीन चयन को रीसेट करें।"), + ("confirm_clear_Wayland_screen_selection_tip", "क्या आप वाकई स्क्रीन चयन साफ़ करना चाहते हैं?"), + ("android_new_voice_call_tip", "नया वॉयस कॉल अनुरोध"), + ("texture_render_tip", "टेक्सचर रेंडरिंग का उपयोग करें"), + ("Use texture rendering", "टेक्सचर रेंडरिंग का उपयोग करें"), + ("Floating window", "फ्लोटिंग विंडो"), + ("floating_window_tip", "बैकग्राउंड में रहने के दौरान RustDesk को दिखाएं"), + ("Keep screen on", "स्क्रीन चालू रखें"), + ("Never", "कभी नहीं"), + ("During controlled", "नियंत्रण के दौरान"), + ("During service is on", "जब सेवा चालू हो"), + ("Capture screen using DirectX", "DirectX का उपयोग करके स्क्रीन कैप्चर करें"), + ("Back", "पीछे"), + ("Apps", "ऐप्स"), + ("Volume up", "आवाज़ बढ़ाएं"), + ("Volume down", "आवाज़ कम करें"), + ("Power", "पावर"), + ("Telegram bot", "Telegram बॉट"), + ("enable-bot-tip", "सूचनाओं के लिए बोट सक्षम करें"), + ("enable-bot-desc", "निर्देशों के लिए हमारे टेलीग्राम बोट को देखें।"), + ("cancel-2fa-confirm-tip", "क्या आप वाकई 2FA रद्द करना चाहते हैं?"), + ("cancel-bot-confirm-tip", "क्या आप वाकई बोट रद्द करना चाहते हैं?"), + ("About RustDesk", "RustDesk के बारे में"), + ("Send clipboard keystrokes", "क्लिपबोर्ड कीस्ट्रोक्स भेजें"), + ("network_error_tip", "नेटवर्क कनेक्शन त्रुटि, कृपया पुनः प्रयास करें।"), + ("Unlock with PIN", "PIN से अनलॉक करें"), + ("Requires at least {} characters", "कम से कम {} अक्षरों की आवश्यकता है"), + ("Wrong PIN", "गलत PIN"), + ("Set PIN", "PIN सेट करें"), + ("Enable trusted devices", "विश्वसनीय डिवाइस सक्षम करें"), + ("Manage trusted devices", "विश्वसनीय डिवाइस प्रबंधित करें"), + ("Platform", "प्लेटफ़ॉर्म"), + ("Days remaining", "शेष दिन"), + ("enable-trusted-devices-tip", "केवल विश्वसनीय डिवाइस ही पासवर्ड के बिना जुड़ सकते हैं"), + ("Parent directory", "पैरेंट निर्देशिका"), + ("Resume", "फिर से शुरू करें"), + ("Invalid file name", "अमान्य फ़ाइल नाम"), + ("one-way-file-transfer-tip", "केवल एकतरफा फ़ाइल स्थानांतरण की अनुमति है"), + ("Authentication Required", "प्रमाणीकरण आवश्यक"), + ("Authenticate", "प्रमाणित करें"), + ("web_id_input_tip", "रिमोट आईडी दर्ज करें"), + ("Download", "डाउनलोड करें"), + ("Upload folder", "फ़ोल्डर अपलोड करें"), + ("Upload files", "फाइलें अपलोड करें"), + ("Clipboard is synchronized", "क्लिपबोर्ड सिंक हो गया है"), + ("Update client clipboard", "क्लाइंट क्लिपबोर्ड अपडेट करें"), + ("Untagged", "बिना टैग वाला"), + ("new-version-of-{}-tip", "{} का नया संस्करण उपलब्ध है"), + ("Accessible devices", "सुलभ डिवाइस"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "रिमोट RustDesk क्लाइंट को संस्करण {} में अपग्रेड करें"), + ("d3d_render_tip", "D3D रेंडरिंग का उपयोग करें"), + ("Printer", "प्रिंटर"), + ("printer-os-requirement-tip", "प्रिंटिंग के लिए Windows आवश्यक है।"), + ("printer-requires-installed-{}-client-tip", "इसके लिए क्लाइंट साइड पर {} इंस्टॉल होना चाहिए।"), + ("printer-{}-not-installed-tip", "प्रिंटर {} इंस्टॉल नहीं है।"), + ("printer-{}-ready-tip", "प्रिंटर {} तैयार है।"), + ("Install {} Printer", "{} प्रिंटर इंस्टॉल करें"), + ("Outgoing Print Jobs", "आउटगोइंग प्रिंट कार्य"), + ("Incoming Print Jobs", "इनकमिंग प्रिंट कार्य"), + ("Incoming Print Job", "इनकमिंग प्रिंट कार्य"), + ("use-the-default-printer-tip", "डिफ़ॉल्ट प्रिंटर का उपयोग करें"), + ("use-the-selected-printer-tip", "चयनित प्रिंटर का उपयोग करें"), + ("auto-print-tip", "स्वचालित रूप से प्रिंट करें"), + ("print-incoming-job-confirm-tip", "प्रिंट कार्य स्वीकार करने से पहले पुष्टि करें"), + ("remote-printing-disallowed-tile-tip", "रिमोट प्रिंटिंग की अनुमति नहीं है"), + ("remote-printing-disallowed-text-tip", "कृपया सेटिंग्स में रिमोट प्रिंटिंग सक्षम करें।"), + ("save-settings-tip", "सेटिंग्स सुरक्षित करें"), + ("dont-show-again-tip", "दोबारा न दिखाएं"), + ("Take screenshot", "स्क्रीनशॉट लें"), + ("Taking screenshot", "स्क्रीनशॉट लिया जा रहा है"), + ("screenshot-merged-screen-not-supported-tip", "मर्ज की गई स्क्रीन के स्क्रीनशॉट समर्थित नहीं हैं।"), + ("screenshot-action-tip", "स्क्रीनशॉट लेने के बाद की कार्रवाई"), + ("Save as", "इस रूप में सहेजें"), + ("Copy to clipboard", "क्लिपबोर्ड पर कॉपी करें"), + ("Enable remote printer", "रिमोट प्रिंटर सक्षम करें"), + ("Downloading {}", "{} डाउनलोड हो रहा है"), + ("{} Update", "{} अपडेट"), + ("{}-to-update-tip", "अपडेट करने के लिए {}"), + ("download-new-version-failed-tip", "नया संस्करण डाउनलोड करने में विफल।"), + ("Auto update", "ऑटो अपडेट"), + ("update-failed-check-msi-tip", "अपडेट विफल, कृपया MSI फ़ाइल की जांच करें।"), + ("websocket_tip", "यदि पोर्ट ब्लॉक हैं, तो WebSocket का उपयोग करें।"), + ("Use WebSocket", "WebSocket का उपयोग करें"), + ("Trackpad speed", "ट्रैकपैड गति"), + ("Default trackpad speed", "डिफ़ॉल्ट ट्रैकपैड गति"), + ("Numeric one-time password", "संख्यात्मक वन-टाइम पासवर्ड"), + ("Enable IPv6 P2P connection", "IPv6 P2P कनेक्शन सक्षम करें"), + ("Enable UDP hole punching", "UDP होल पंचिंग सक्षम करें"), + ("View camera", "कैमरा देखें"), + ("Enable camera", "कैमरा सक्षम करें"), + ("No cameras", "कोई कैमरा नहीं मिला"), + ("view_camera_unsupported_tip", "रिमोट कैमरा समर्थित नहीं है।"), + ("Terminal", "टर्मिनल"), + ("Enable terminal", "टर्मिनल सक्षम करें"), + ("New tab", "नया टैब"), + ("Keep terminal sessions on disconnect", "डिस्कनेक्ट होने पर टर्मिनल सत्र चालू रखें"), + ("Terminal (Run as administrator)", "टर्मिनल (प्रशासक के रूप में चलाएं)"), + ("terminal-admin-login-tip", "प्रशासक लॉगिन आवश्यक है।"), + ("Failed to get user token.", "उपयोगकर्ता टोकन प्राप्त करने में विफल।"), + ("Incorrect username or password.", "गलत उपयोगकर्ता नाम या पासवर्ड।"), + ("The user is not an administrator.", "उपयोगकर्ता प्रशासक नहीं है।"), + ("Failed to check if the user is an administrator.", "जांचने में विफल कि क्या उपयोगकर्ता व्यवस्थापक है।"), + ("Supported only in the installed version.", "केवल इंस्टॉल किए गए संस्करण में समर्थित।"), + ("elevation_username_tip", "प्रशासक उपयोगकर्ता नाम दर्ज करें"), + ("Preparing for installation ...", "स्थापना की तैयारी..."), + ("Show my cursor", "मेरा कर्सर दिखाएं"), + ("Scale custom", "कस्टम पैमाना"), + ("Custom scale slider", "कस्टम स्केल स्लाइडर"), + ("Decrease", "घटाएं"), + ("Increase", "बढ़ाएं"), + ("Show virtual mouse", "वर्चुअल माउस दिखाएं"), + ("Virtual mouse size", "वर्चुअल माउस का आकार"), + ("Small", "छोटा"), + ("Large", "बड़ा"), + ("Show virtual joystick", "वर्चुअल जॉयस्टिक दिखाएं"), + ("Edit note", "नोट संपादित करें"), + ("Alias", "उपनाम (Alias)"), + ("ScrollEdge", "किनारे से स्क्रॉल"), + ("Allow insecure TLS fallback", "असुरक्षित TLS फ़ालबैक की अनुमति दें"), + ("allow-insecure-tls-fallback-tip", "पुराने सर्वर कनेक्शन के लिए उपयोग करें।"), + ("Disable UDP", "UDP अक्षम करें"), + ("disable-udp-tip", "कनेक्शन समस्याओं के लिए UDP बंद करें।"), + ("server-oss-not-support-tip", "OSS सर्वर इसका समर्थन नहीं करता।"), + ("input note here", "यहाँ नोट दर्ज करें"), + ("note-at-conn-end-tip", "कनेक्शन के अंत में नोट दिखाएं"), + ("Show terminal extra keys", "टर्मिनल की अतिरिक्त कुंजियाँ दिखाएं"), + ("Relative mouse mode", "सापेक्ष (Relative) माउस मोड"), + ("rel-mouse-not-supported-peer-tip", "रिमोट साइड पर समर्थित नहीं है।"), + ("rel-mouse-not-ready-tip", "तैयार नहीं है।"), + ("rel-mouse-lock-failed-tip", "माउस लॉक विफल।"), + ("rel-mouse-exit-{}-tip", "बाहर निकलने के लिए {} दबाएं"), + ("rel-mouse-permission-lost-tip", "अनुमति खो गई।"), + ("Changelog", "परिवर्तन सूची (Changelog)"), + ("keep-awake-during-outgoing-sessions-label", "आउटगोइंग सत्र के दौरान जागते रहें"), + ("keep-awake-during-incoming-sessions-label", "इनकमिंग सत्र के दौरान जागते रहें"), + ("Continue with {}", "{} के साथ जारी रखें"), + ("Display Name", "प्रदर्शित नाम"), + ("password-hidden-tip", "पासवर्ड सुरक्षा के लिए छिपा हुआ है।"), + ("preset-password-in-use-tip", "पूर्व-निर्धारित पासवर्ड उपयोग में है।"), + ].iter().cloned().collect(); +} From 9bc1ce52af263f68e7fd24d1f3e51227970ad6bb Mon Sep 17 00:00:00 2001 From: Leo Louis Date: Wed, 22 Apr 2026 15:36:10 +0530 Subject: [PATCH 226/277] Add Malayalam language support (#14753) * Add Malayalam language support * Fix syntax error in language list for Malayalam --------- Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> --- src/lang.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lang.rs b/src/lang.rs index 682a4a51a..6302c2aed 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -49,6 +49,7 @@ mod vi; mod ta; mod ge; mod fi; +mod ml; pub const LANGS: &[(&str, &str)] = &[ ("en", "English"), @@ -97,6 +98,7 @@ pub const LANGS: &[(&str, &str)] = &[ ("ta", "தமிழ்"), ("ge", "ქართული"), ("fi", "Suomi"), + ("ml", "മലയാളം"), ("hi", "हिंदी"), ("gu", "ગુજરાતી"), ]; @@ -177,6 +179,7 @@ pub fn translate_locale(name: String, locale: &str) -> String { "sc" => sc::T.deref(), "ta" => ta::T.deref(), "ge" => ge::T.deref(), + "ml" => ml::T.deref(), "hi" => hi::T.deref(), "gu" => gu::T.deref(), _ => en::T.deref(), From 47e4c65d8e888b9d27e33bd596f6e8ca4d69e451 Mon Sep 17 00:00:00 2001 From: Leo Louis Date: Wed, 22 Apr 2026 15:36:37 +0530 Subject: [PATCH 227/277] Update print statement from 'Hello' to 'Goodbye' (#14754) --- src/lang/ml.rs | 746 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 746 insertions(+) create mode 100644 src/lang/ml.rs diff --git a/src/lang/ml.rs b/src/lang/ml.rs new file mode 100644 index 000000000..099f1d385 --- /dev/null +++ b/src/lang/ml.rs @@ -0,0 +1,746 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "നില"), + ("Your Desktop", "നിങ്ങളുടെ ഡെസ്ക്ടോപ്പ്"), + ("desk_tip", "ഈ ഐഡിയും പാസ്‌വേഡും ഉപയോഗിച്ച് നിങ്ങളുടെ ഡെസ്ക്ടോപ്പ് ആക്‌സസ് ചെയ്യാം."), + ("Password", "പാസ്‌വേഡ്"), + ("Ready", "തയ്യാറാണ്"), + ("Established", "ബന്ധം സ്ഥാപിച്ചു"), + ("connecting_status", "നെറ്റ്‌വർക്കുമായി ബന്ധിപ്പിക്കുന്നു..."), + ("Enable service", "സർവീസ് പ്രവർത്തനക്ഷമമാക്കുക"), + ("Start service", "സർവീസ് തുടങ്ങുക"), + ("Service is running", "സർവീസ് പ്രവർത്തിക്കുന്നു"), + ("Service is not running", "സർവീസ് പ്രവർത്തിക്കുന്നില്ല"), + ("not_ready_status", "തയ്യാറായിട്ടില്ല. ദയവായി നിങ്ങളുടെ കണക്ഷൻ പരിശോധിക്കുക"), + ("Control Remote Desktop", "റിമോട്ട് ഡെസ്ക്ടോപ്പ് നിയന്ത്രിക്കുക"), + ("Transfer file", "ഫയൽ കൈമാറുക"), + ("Connect", "കണക്ട് ചെയ്യുക"), + ("Recent sessions", "സമീപകാല സെഷനുകൾ"), + ("Address book", "അഡ്രസ് ബുക്ക്"), + ("Confirmation", "സ്ഥിരീകരണം"), + ("TCP tunneling", "TCP ടണലിംഗ്"), + ("Remove", "നീക്കം ചെയ്യുക"), + ("Refresh random password", "പുതിയ പാസ്‌വേഡ് ജനറേറ്റ് ചെയ്യുക"), + ("Set your own password", "സ്വന്തം പാസ്‌വേഡ് സെറ്റ് ചെയ്യുക"), + ("Enable keyboard/mouse", "കീബോർഡ്/മൗസ് അനുവദിക്കുക"), + ("Enable clipboard", "ക്ലിപ്പ്ബോർഡ് അനുവദിക്കുക"), + ("Enable file transfer", "ഫയൽ കൈമാറ്റം അനുവദിക്കുക"), + ("Enable TCP tunneling", "TCP ടണലിംഗ് അനുവദിക്കുക"), + ("IP Whitelisting", "IP വൈറ്റ്‌ലിസ്റ്റിംഗ്"), + ("ID/Relay Server", "ID/റിലേ സെർവർ"), + ("Import server config", "സെർവർ കോൺഫിഗറേഷൻ ഇമ്പോർട്ട് ചെയ്യുക"), + ("Export Server Config", "സെർവർ കോൺഫിഗറേഷൻ എക്‌സ്‌പോർട്ട് ചെയ്യുക"), + ("Import server configuration successfully", "സെർവർ കോൺഫിഗറേഷൻ വിജയകരമായി ഇമ്പോർട്ട് ചെയ്തു"), + ("Export server configuration successfully", "സെർവർ കോൺഫിഗറേഷൻ വിജയകരമായി എക്‌സ്‌പോർട്ട് ചെയ്തു"), + ("Invalid server configuration", "അസാധുവായ സെർവർ കോൺഫിഗറേഷൻ"), + ("Clipboard is empty", "ക്ലിപ്പ്ബോർഡ് ശൂന്യമാണ്"), + ("Stop service", "സർവീസ് നിർത്തുക"), + ("Change ID", "ഐഡി മാറ്റുക"), + ("Your new ID", "നിങ്ങളുടെ പുതിയ ഐഡി"), + ("length %min% to %max%", "നീളം %min% മുതൽ %max% വരെ"), + ("starts with a letter", "അക്ഷരത്തിൽ തുടങ്ങണം"), + ("allowed characters", "അനുവദനീയമായ അക്ഷരങ്ങൾ"), + ("id_change_tip", "ഐഡി മാറ്റിയാൽ നിലവിലുള്ള കണക്ഷൻ വിച്ഛേദിക്കപ്പെടും."), + ("Website", "വെബ്സൈറ്റ്"), + ("About", "വിവരങ്ങൾ"), + ("Slogan_tip", "മികച്ച അനുഭവത്തിനായി നിർമ്മിച്ച റിമോട്ട് ഡെസ്ക്ടോപ്പ് സോഫ്റ്റ്‌വെയർ"), + ("Privacy Statement", "സ്വകാര്യതാ പ്രസ്താവന"), + ("Mute", "നിശബ്ദമാക്കുക"), + ("Build Date", "നിർമ്മാണ തീയതി"), + ("Version", "പതിപ്പ്"), + ("Home", "ഹോം"), + ("Audio Input", "ഓഡിയോ ഇൻപുട്ട്"), + ("Enhancements", "മെച്ചപ്പെടുത്തലുകൾ"), + ("Hardware Codec", "ഹാർഡ്‌വെയർ കോഡെക്"), + ("Adaptive bitrate", "അഡാപ്റ്റീവ് ബിറ്റ്റേറ്റ്"), + ("ID Server", "ID സെർവർ"), + ("Relay Server", "റിലേ സെർവർ"), + ("API Server", "API സെർവർ"), + ("invalid_http", "അസാധുവായ HTTP ലിങ്ക്"), + ("Invalid IP", "അസാധുവായ IP"), + ("Invalid format", "അസാധുവായ ഫോർമാറ്റ്"), + ("server_not_support", "സെർവർ പിന്തുണയ്ക്കുന്നില്ല"), + ("Not available", "ലഭ്യമല്ല"), + ("Too frequent", "അമിതമായ തവണകൾ"), + ("Cancel", "റദ്ദാക്കുക"), + ("Skip", "ഒഴിവാക്കുക"), + ("Close", "അടയ്ക്കുക"), + ("Retry", "വീണ്ടും ശ്രമിക്കുക"), + ("OK", "ശരി"), + ("Password Required", "പാസ്‌വേഡ് ആവശ്യമാണ്"), + ("Please enter your password", "ദയവായി നിങ്ങളുടെ പാസ്‌വേഡ് നൽകുക"), + ("Remember password", "പാസ്‌വേഡ് ഓർമ്മിക്കുക"), + ("Wrong Password", "തെറ്റായ പാസ്‌വേഡ്"), + ("Do you want to enter again?", "നിങ്ങൾക്ക് വീണ്ടും ശ്രമിക്കണോ?"), + ("Connection Error", "കണക്ഷൻ പിശക്"), + ("Error", "പിശക്"), + ("Reset by the peer", "മറുഭാഗത്തുനിന്ന് റീസെറ്റ് ചെയ്തു"), + ("Connecting...", "ബന്ധിപ്പിക്കുന്നു..."), + ("Connection in progress. Please wait.", "കണക്ഷൻ നടക്കുന്നു. ദയവായി കാത്തിരിക്കുക."), + ("Please try 1 minute later", "ദയവായി ഒരു മിനിറ്റിന് ശേഷം ശ്രമിക്കുക"), + ("Login Error", "ലോഗിൻ പിശക്"), + ("Successful", "വിജയിച്ചു"), + ("Connected, waiting for image...", "ബന്ധിപ്പിച്ചു, ചിത്രത്തിനായി കാത്തിരിക്കുന്നു..."), + ("Name", "പേര്"), + ("Type", "തരം"), + ("Modified", "മാറ്റം വരുത്തിയത്"), + ("Size", "വലിപ്പം"), + ("Show Hidden Files", "മറഞ്ഞിരിക്കുന്ന ഫയലുകൾ കാണിക്കുക"), + ("Receive", "സ്വീകരിക്കുക"), + ("Send", "അയക്കുക"), + ("Refresh File", "ഫയൽ പുതുക്കുക"), + ("Local", "ലോക്കൽ"), + ("Remote", "റിമോട്ട്"), + ("Remote Computer", "റിമോട്ട് കമ്പ്യൂട്ടർ"), + ("Local Computer", "ലോക്കൽ കമ്പ്യൂട്ടർ"), + ("Confirm Delete", "ഡിലീറ്റ് ചെയ്യുന്നത് സ്ഥിരീകരിക്കുക"), + ("Delete", "ഡിലീറ്റ് ചെയ്യുക"), + ("Properties", "പ്രോപ്പർട്ടീസ്"), + ("Multi Select", "ഒന്നിലധികം തിരഞ്ഞെടുക്കുക"), + ("Select All", "എല്ലാം തിരഞ്ഞെടുക്കുക"), + ("Unselect All", "തിരഞ്ഞെടുത്തവ ഒഴിവാക്കുക"), + ("Empty Directory", "ശൂന്യമായ ഡയറക്ടറി"), + ("Not an empty directory", "ഡയറക്ടറി ശൂന്യമല്ല"), + ("Are you sure you want to delete this file?", "ഈ ഫയൽ ഡിലീറ്റ് ചെയ്യണമെന്ന് നിങ്ങൾക്ക് ഉറപ്പാണോ?"), + ("Are you sure you want to delete this empty directory?", "ഈ ശൂന്യമായ ഡയറക്ടറി ഡിലീറ്റ് ചെയ്യണമെന്ന് നിങ്ങൾക്ക് ഉറപ്പാണോ?"), + ("Are you sure you want to delete the file of this directory?", "ഈ ഡയറക്ടറിയിലെ ഫയലുകൾ ഡിലീറ്റ് ചെയ്യണമെന്ന് നിങ്ങൾക്ക് ഉറപ്പാണോ?"), + ("Do this for all conflicts", "എല്ലാ വൈരുദ്ധ്യങ്ങൾക്കും ഇതുതന്നെ ചെയ്യുക"), + ("This is irreversible!", "ഇത് പഴയപടിയാക്കാൻ കഴിയില്ല!"), + ("Deleting", "ഡിലീറ്റ് ചെയ്യുന്നു"), + ("files", "ഫയലുകൾ"), + ("Waiting", "കാത്തിരിക്കുന്നു"), + ("Finished", "പൂർത്തിയായി"), + ("Speed", "വേഗത"), + ("Custom Image Quality", "ഇമേജ് ക്വാളിറ്റി മാറ്റുക"), + ("Privacy mode", "സ്വകാര്യ മോഡ്"), + ("Block user input", "യൂസർ ഇൻപുട്ട് തടയുക"), + ("Unblock user input", "യൂസർ ഇൻപുട്ട് അനുവദിക്കുക"), + ("Adjust Window", "വിൻഡോ ക്രമീകരിക്കുക"), + ("Original", "ഒറിജിനൽ"), + ("Shrink", "ചുരുക്കുക"), + ("Stretch", "വലിപ്പിക്കുക"), + ("Scrollbar", "സ്ക്രോൾബാർ"), + ("ScrollAuto", "ഓട്ടോ സ്ക്രോൾ"), + ("Good image quality", "നല്ല ക്വാളിറ്റി"), + ("Balanced", "സന്തുലിതം"), + ("Optimize reaction time", "പ്രതികരണ സമയം മെച്ചപ്പെടുത്തുക"), + ("Custom", "കസ്റ്റം"), + ("Show remote cursor", "റിമോട്ട് കർസർ കാണിക്കുക"), + ("Show quality monitor", "ക്വാളിറ്റി മോണിറ്റർ കാണിക്കുക"), + ("Disable clipboard", "ക്ലിപ്പ്ബോർഡ് ഒഴിവാക്കുക"), + ("Lock after session end", "സെഷൻ കഴിഞ്ഞാൽ ലോക്ക് ചെയ്യുക"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del നൽകുക"), + ("Insert Lock", "ലോക്ക് ചെയ്യുക"), + ("Refresh", "പുതുക്കുക"), + ("ID does not exist", "ഐഡി നിലവിലില്ല"), + ("Failed to connect to rendezvous server", "സെർവറുമായി ബന്ധിപ്പിക്കുന്നതിൽ പരാജയപ്പെട്ടു"), + ("Please try later", "ദയവായി പിന്നീട് ശ്രമിക്കുക"), + ("Remote desktop is offline", "റിമോട്ട് ഡെസ്ക്ടോപ്പ് ഓഫ്‌ലൈനാണ്"), + ("Key mismatch", "കീ പൊരുത്തക്കേട്"), + ("Timeout", "സമയം കഴിഞ്ഞു"), + ("Failed to connect to relay server", "റിലേ സെർവറുമായി ബന്ധിപ്പിക്കുന്നതിൽ പരാജയപ്പെട്ടു"), + ("Failed to connect via rendezvous server", "സെർവർ വഴി ബന്ധിപ്പിക്കുന്നതിൽ പരാജയപ്പെട്ടു"), + ("Failed to connect via relay server", "റിലേ സെർവർ വഴി ബന്ധിപ്പിക്കുന്നതിൽ പരാജയപ്പെട്ടു"), + ("Failed to make direct connection to remote desktop", "നേരിട്ട് ബന്ധിപ്പിക്കുന്നതിൽ പരാജയപ്പെട്ടു"), + ("Set Password", "പാസ്‌വേഡ് നൽകുക"), + ("OS Password", "OS പാസ്‌വേഡ്"), + ("install_tip", "മികച്ച പ്രകടനത്തിനായി ഇൻസ്റ്റാൾ ചെയ്യുക."), + ("Click to upgrade", "അപ്‌ഗ്രേഡ് ചെയ്യാൻ ക്ലിക്ക് ചെയ്യുക"), + ("Configure", "ക്രമീകരിക്കുക"), + ("config_acc", "അക്‌സസിബിലിറ്റി ക്രമീകരിക്കുക"), + ("config_screen", "സ്ക്രീൻ ക്രമീകരിക്കുക"), + ("Installing ...", "ഇൻസ്റ്റാൾ ചെയ്യുന്നു..."), + ("Install", "ഇൻസ്റ്റാൾ ചെയ്യുക"), + ("Installation", "ഇൻസ്റ്റാളേഷൻ"), + ("Installation Path", "ഇൻസ്റ്റാളേഷൻ പാത്ത്"), + ("Create start menu shortcuts", "സ്റ്റാർട്ട് മെനുവിൽ ഷോർട്ട്കട്ട് ഉണ്ടാക്കുക"), + ("Create desktop icon", "ഡെസ്ക്ടോപ്പ് ഐക്കൺ ഉണ്ടാക്കുക"), + ("agreement_tip", "ഇൻസ്റ്റാൾ ചെയ്യുന്നതിലൂടെ നിങ്ങൾ കരാറുകൾ അംഗീകരിക്കുന്നു."), + ("Accept and Install", "അംഗീകരിച്ച് ഇൻസ്റ്റാൾ ചെയ്യുക"), + ("End-user license agreement", "ലൈസൻസ് കരാർ"), + ("Generating ...", "ഉണ്ടാക്കുന്നു..."), + ("Your installation is lower version.", "നിങ്ങളുടെ ഇൻസ്റ്റാളേഷൻ പഴയ പതിപ്പാണ്."), + ("not_close_tcp_tip", "ടണൽ ഉപയോഗിക്കുമ്പോൾ ഈ വിൻഡോ അടയ്ക്കരുത്."), + ("Listening ...", "ശ്രദ്ധിക്കുന്നു..."), + ("Remote Host", "റിമോട്ട് ഹോസ്റ്റ്"), + ("Remote Port", "റിമോട്ട് പോർട്ട്"), + ("Action", "നടപടി"), + ("Add", "ചേർക്കുക"), + ("Local Port", "ലോക്കൽ പോർട്ട്"), + ("Local Address", "ലോക്കൽ അഡ്രസ്"), + ("Change Local Port", "ലോക്കൽ പോർട്ട് മാറ്റുക"), + ("setup_server_tip", "വേഗതയുള്ള കണക്ഷനായി സ്വന്തം സെർവർ സജ്ജമാക്കുക"), + ("Too short, at least 6 characters.", "വളരെ ചെറുതാണ്, കുറഞ്ഞത് 6 അക്ഷരങ്ങൾ വേണം."), + ("The confirmation is not identical.", "സ്ഥിരീകരണം ഒരേപോലെയല്ല."), + ("Permissions", "അനുമതികൾ"), + ("Accept", "സ്വീകരിക്കുക"), + ("Dismiss", "നിരസിക്കുക"), + ("Disconnect", "വിച്ഛേദിക്കുക"), + ("Enable file copy and paste", "ഫയൽ കോപ്പി-പേസ്റ്റ് അനുവദിക്കുക"), + ("Connected", "ബന്ധിപ്പിച്ചു"), + ("Direct and encrypted connection", "നേരിട്ടുള്ളതും എൻക്രിപ്റ്റ് ചെയ്തതുമായ കണക്ഷൻ"), + ("Relayed and encrypted connection", "റിലേ വഴിയുള്ള എൻക്രിപ്റ്റ് ചെയ്ത കണക്ഷൻ"), + ("Direct and unencrypted connection", "നേരിട്ടുള്ളതും എൻക്രിപ്റ്റ് ചെയ്യാത്തതുമായ കണക്ഷൻ"), + ("Relayed and unencrypted connection", "റിലേ വഴിയുള്ള എൻക്രിപ്റ്റ് ചെയ്യാത്ത കണക്ഷൻ"), + ("Enter Remote ID", "റിമോട്ട് ഐഡി നൽകുക"), + ("Enter your password", "നിങ്ങളുടെ പാസ്‌വേഡ് നൽകുക"), + ("Logging in...", "ലോഗിൻ ചെയ്യുന്നു..."), + ("Enable RDP session sharing", "RDP സെഷൻ പങ്കിടൽ അനുവദിക്കുക"), + ("Auto Login", "ഓട്ടോ ലോഗിൻ"), + ("Enable direct IP access", "നേരിട്ടുള്ള IP ആക്‌സസ് അനുവദിക്കുക"), + ("Rename", "പേര് മാറ്റുക"), + ("Space", "സ്പേസ്"), + ("Create desktop shortcut", "ഡെസ്ക്ടോപ്പ് ഷോർട്ട്കട്ട് ഉണ്ടാക്കുക"), + ("Change Path", "പാത്ത് മാറ്റുക"), + ("Create Folder", "ഫോൾഡർ ഉണ്ടാക്കുക"), + ("Please enter the folder name", "ദയവായി ഫോൾഡറിന്റെ പേര് നൽകുക"), + ("Fix it", "പരിഹരിക്കുക"), + ("Warning", "മുന്നറിയിപ്പ്"), + ("Login screen using Wayland is not supported", "Wayland വഴിയുള്ള ലോഗിൻ സപ്പോർട്ട് ചെയ്യുന്നില്ല"), + ("Reboot required", "റീബൂട്ട് ആവശ്യമാണ്"), + ("Unsupported display server", "പിന്തുണയ്ക്കാത്ത ഡിസ്‌പ്ലേ സെർവർ"), + ("x11 expected", "x11 ആവശ്യമാണ്"), + ("Port", "പോർട്ട്"), + ("Settings", "ക്രമീകരണങ്ങൾ"), + ("Username", "യൂസർ നെയിം"), + ("Invalid port", "അസാധുവായ പോർട്ട്"), + ("Closed manually by the peer", "മറുഭാഗത്തുനിന്നും മാനുവലായി അടച്ചു"), + ("Enable remote configuration modification", "റിമോട്ട് കോൺഫിഗറേഷൻ മാറ്റങ്ങൾ അനുവദിക്കുക"), + ("Run without install", "ഇൻസ്റ്റാൾ ചെയ്യാതെ പ്രവർത്തിപ്പിക്കുക"), + ("Connect via relay", "റിലേ വഴി കണക്ട് ചെയ്യുക"), + ("Always connect via relay", "എപ്പോഴും റിലേ വഴി കണക്ട് ചെയ്യുക"), + ("whitelist_tip", "വൈറ്റ്‌ലിസ്റ്റ് ചെയ്ത ഐപികൾക്ക് മാത്രമേ എന്നെ ആക്‌സസ് ചെയ്യാൻ കഴിയൂ"), + ("Login", "ലോഗിൻ"), + ("Verify", "പരിശോധിക്കുക"), + ("Remember me", "എന്നെ ഓർമ്മിക്കുക"), + ("Trust this device", "ഈ ഉപകരണം വിശ്വസിക്കുക"), + ("Verification code", "വെരിഫിക്കേഷൻ കോഡ്"), + ("verification_tip", "വെരിഫിക്കേഷൻ കോഡ് നിങ്ങളുടെ ഇമെയിലിലേക്ക് അയച്ചു"), + ("Logout", "ലോഗൗട്ട്"), + ("Tags", "ടാഗുകൾ"), + ("Search ID", "ഐഡി തിരയുക"), + ("whitelist_sep", "കോമ, സെമി കോളൻ അല്ലെങ്കിൽ സ്പേസ് ഉപയോഗിച്ച് തിരിക്കുക"), + ("Add ID", "ഐഡി ചേർക്കുക"), + ("Add Tag", "ടാഗ് ചേർക്കുക"), + ("Unselect all tags", "എല്ലാ ടാഗുകളും ഒഴിവാക്കുക"), + ("Network error", "നെറ്റ്‌വർക്ക് പിശക്"), + ("Username missed", "യൂസർ നെയിം നൽകിയില്ല"), + ("Password missed", "പാസ്‌വേഡ് നൽകിയില്ല"), + ("Wrong credentials", "തെറ്റായ വിവരങ്ങൾ"), + ("The verification code is incorrect or has expired", "കോഡ് തെറ്റാണ് അല്ലെങ്കിൽ കാലാവധി കഴിഞ്ഞു"), + ("Edit Tag", "ടാഗ് മാറ്റുക"), + ("Forget Password", "പാസ്‌വേഡ് മറന്നു"), + ("Favorites", "പ്രിയപ്പെട്ടവ"), + ("Add to Favorites", "പ്രിയപ്പെട്ടവയിലേക്ക് ചേർക്കുക"), + ("Remove from Favorites", "പ്രിയപ്പെട്ടവയിൽ നിന്ന് നീക്കം ചെയ്യുക"), + ("Empty", "ശൂന്യം"), + ("Invalid folder name", "അസാധുവായ ഫോൾഡർ പേര്"), + ("Socks5 Proxy", "Socks5 പ്രോക്സി"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) പ്രോക്സി"), + ("Discovered", "കണ്ടെത്തിയവ"), + ("install_daemon_tip", "കമ്പ്യൂട്ടർ തുടങ്ങുമ്പോൾ തന്നെ പ്രവർത്തിക്കാൻ സർവീസ് ഇൻസ്റ്റാൾ ചെയ്യുക"), + ("Remote ID", "റിമോട്ട് ഐഡി"), + ("Paste", "പേസ്റ്റ്"), + ("Paste here?", "ഇവിടെ പേസ്റ്റ് ചെയ്യണോ?"), + ("Are you sure to close the connection?", "കണക്ഷൻ നിർത്തണമെന്ന് നിങ്ങൾക്ക് ഉറപ്പാണോ?"), + ("Download new version", "പുതിയ പതിപ്പ് ഡൗൺലോഡ് ചെയ്യുക"), + ("Touch mode", "ടച്ച് മോഡ്"), + ("Mouse mode", "മൗസ് മോഡ്"), + ("One-Finger Tap", "ഒരു വിരൽ ടാപ്പ്"), + ("Left Mouse", "മൗസ് ഇടത് ബട്ടൺ"), + ("One-Long Tap", "ഒരു നീണ്ട ടാപ്പ്"), + ("Two-Finger Tap", "രണ്ട് വിരൽ ടാപ്പ്"), + ("Right Mouse", "മൗസ് വലത് ബട്ടൺ"), + ("One-Finger Move", "ഒരു വിരൽ നീക്കം"), + ("Double Tap & Move", "രണ്ട് ടാപ്പും നീക്കവും"), + ("Mouse Drag", "മൗസ് ഡ്രാഗ്"), + ("Three-Finger vertically", "മൂന്ന് വിരൽ ലംബമായി"), + ("Mouse Wheel", "മൗസ് വീൽ"), + ("Two-Finger Move", "രണ്ട് വിരൽ നീക്കം"), + ("Canvas Move", "ക്യാൻവാസ് നീക്കുക"), + ("Pinch to Zoom", "സൂം ചെയ്യാൻ പിഞ്ച് ചെയ്യുക"), + ("Canvas Zoom", "ക്യാൻവാസ് സൂം"), + ("Reset canvas", "ക്യാൻവാസ് റീസെറ്റ് ചെയ്യുക"), + ("No permission of file transfer", "ഫയൽ കൈമാറ്റത്തിന് അനുമതിയില്ല"), + ("Note", "കുറിപ്പ്"), + ("Connection", "കണക്ഷൻ"), + ("Share screen", "സ്ക്രീൻ പങ്കിടുക"), + ("Chat", "ചാറ്റ്"), + ("Total", "ആകെ"), + ("items", "ഇനങ്ങൾ"), + ("Selected", "തിഞ്ഞെടുത്തവ"), + ("Screen Capture", "സ്ക്രീൻ ക്യാപ്ചർ"), + ("Input Control", "ഇൻപുട്ട് നിയന്ത്രണം"), + ("Audio Capture", "ഓഡിയോ ക്യാപ്ചർ"), + ("Do you accept?", "നിങ്ങൾ അംഗീകരിക്കുന്നുണ്ടോ?"), + ("Open System Setting", "സിസ്റ്റം സെറ്റിംഗ്സ് തുറക്കുക"), + ("How to get Android input permission?", "ആൻഡ്രോയിഡ് ഇൻപുട്ട് അനുമതി എങ്ങനെ നേടാം?"), + ("android_input_permission_tip1", "ഇൻപുട്ട് അനുമതിക്കായി ആക്‌സസിബിലിറ്റി സർവീസ് ഓൺ ചെയ്യുക."), + ("android_input_permission_tip2", "സെറ്റിംഗ്സിൽ RustDesk കണ്ടെത്തി അത് ഓൺ ചെയ്യുക."), + ("android_new_connection_tip", "പുതിയ കണക്ഷൻ അഭ്യർത്ഥന ലഭിച്ചു."), + ("android_service_will_start_tip", "സ്ക്രീൻ ക്യാപ്ചർ ഓൺ ചെയ്താൽ സർവീസ് താനേ തുടങ്ങും."), + ("android_stop_service_tip", "സർവീസ് നിർത്തുന്നത് എല്ലാ കണക്ഷനുകളും വിച്ഛേദിക്കും."), + ("android_version_audio_tip", "ആൻഡ്രോയിഡ് 10-ൽ കൂടുതൽ വേണം ഓഡിയോ ക്യാപ്ചർ ചെയ്യാൻ."), + ("android_start_service_tip", "സ്ക്രീൻ ഷെയറിംഗ് തുടങ്ങാൻ ക്ലിക്ക് ചെയ്യുക."), + ("android_permission_may_not_change_tip", "അനുമതികൾ പിന്നീട് മാറ്റാൻ കഴിയില്ല, ശ്രദ്ധിച്ച് തിരഞ്ഞെടുക്കുക."), + ("Account", "അക്കൗണ്ട്"), + ("Overwrite", "തിരുത്തിയെഴുതുക (Overwrite)"), + ("This file exists, skip or overwrite this file?", "ഈ ഫയൽ നിലവിലുണ്ട്, ഒഴിവാക്കണോ അതോ തിരുത്തിയെഴുതണോ?"), + ("Quit", "പുറത്തുകടക്കുക"), + ("Help", "സഹായം"), + ("Failed", "പരാജയപ്പെട്ടു"), + ("Succeeded", "വിജയിച്ചു"), + ("Someone turns on privacy mode, exit", "ആരോ പ്രൈവസി മോഡ് ഓൺ ചെയ്തു, പുറത്തുകടക്കുന്നു"), + ("Unsupported", "പിന്തുണയ്ക്കുന്നില്ല"), + ("Peer denied", "മറുഭാഗത്തുനിന്ന് നിരസിച്ചു"), + ("Please install plugins", "ദയവായി പ്ലഗിനുകൾ ഇൻസ്റ്റാൾ ചെയ്യുക"), + ("Peer exit", "മറുഭാഗത്തുനിന്ന് പുറത്തുകടന്നു"), + ("Failed to turn off", "ഓഫ് ചെയ്യുന്നതിൽ പരാജയപ്പെട്ടു"), + ("Turned off", "ഓഫ് ചെയ്തു"), + ("Language", "ഭാഷ"), + ("Keep RustDesk background service", "RustDesk ബാക്ക്ഗ്രൗണ്ടിൽ പ്രവർത്തിപ്പിക്കുക"), + ("Ignore Battery Optimizations", "ബാറ്ററി ഒപ്റ്റിമൈസേഷൻ അവഗണിക്കുക"), + ("android_open_battery_optimizations_tip", "കണക്ഷൻ മുറിയാതിരിക്കാൻ ബാറ്ററി ഒപ്റ്റിമൈസേഷൻ സെറ്റിംഗ്സ് തുറക്കുക"), + ("Start on boot", "തുടങ്ങുമ്പോൾ തന്നെ പ്രവർത്തിക്കുക"), + ("Start the screen sharing service on boot, requires special permissions", "തുടങ്ങുമ്പോൾ തന്നെ സ്ക്രീൻ ഷെയറിംഗ് തുടങ്ങുക, പ്രത്യേക അനുമതി ആവശ്യമാണ്"), + ("Connection not allowed", "കണക്ഷൻ അനുവദനീയമല്ല"), + ("Legacy mode", "ലെഗസി മോഡ്"), + ("Map mode", "മാപ്പ് മോഡ്"), + ("Translate mode", "ട്രാൻസ്ലേറ്റ് മോഡ്"), + ("Use permanent password", "സ്ഥിരമായ പാസ്‌വേഡ് ഉപയോഗിക്കുക"), + ("Use both passwords", "രണ്ട് പാസ്‌വേഡുകളും ഉപയോഗിക്കുക"), + ("Set permanent password", "സ്ഥിരമായ പാസ്‌വേഡ് സജ്ജമാക്കുക"), + ("Enable remote restart", "റിമോട്ട് റീസ്റ്റാർട്ട് അനുവദിക്കുക"), + ("Restart remote device", "റിമോട്ട് ഉപകരണം റീസ്റ്റാർട്ട് ചെയ്യുക"), + ("Are you sure you want to restart", "റീസ്റ്റാർട്ട് ചെയ്യണമെന്ന് നിങ്ങൾക്ക് ഉറപ്പാണോ?"), + ("Restarting remote device", "റിമോട്ട് ഉപകരണം റീസ്റ്റാർട്ട് ചെയ്യുന്നു"), + ("remote_restarting_tip", "റിമോട്ട് ഉപകരണം റീസ്റ്റാർട്ട് ചെയ്യുന്നു, ദയവായി കാത്തിരിക്കുക..."), + ("Copied", "കോപ്പി ചെയ്തു"), + ("Exit Fullscreen", "ഫുൾ സ്ക്രീനിൽ നിന്ന് പുറത്തുകടക്കുക"), + ("Fullscreen", "ഫുൾ സ്ക്രീൻ"), + ("Mobile Actions", "മൊബൈൽ നടപടികൾ"), + ("Select Monitor", "മോണിറ്റർ തിരഞ്ഞെടുക്കുക"), + ("Control Actions", "നിയന്ത്രണ നടപടികൾ"), + ("Display Settings", "ഡിസ്‌പ്ലേ ക്രമീകരണങ്ങൾ"), + ("Ratio", "അനുപാതം (Ratio)"), + ("Image Quality", "ചിത്രത്തിന്റെ ഗുണനിലവാരം"), + ("Scroll Style", "സ്ക്രോൾ സ്റ്റൈൽ"), + ("Show Toolbar", "ടൂൾബാർ കാണിക്കുക"), + ("Hide Toolbar", "ടൂൾബാർ മറയ്ക്കുക"), + ("Direct Connection", "നേരിട്ടുള്ള കണക്ഷൻ"), + ("Relay Connection", "റിലേ കണക്ഷൻ"), + ("Secure Connection", "സുരക്ഷിതമായ കണക്ഷൻ"), + ("Insecure Connection", "സുരക്ഷിതമല്ലാത്ത കണക്ഷൻ"), + ("Scale original", "ഒറിജിനൽ വലിപ്പം"), + ("Scale adaptive", "അഡാപ്റ്റീവ് വലിപ്പം"), + ("General", "പൊതുവായവ"), + ("Security", "സുരക്ഷ"), + ("Theme", "തീം"), + ("Dark Theme", "ഡാർക്ക് തീം"), + ("Light Theme", "ലൈറ്റ് തീം"), + ("Dark", "ഡാർക്ക്"), + ("Light", "ലൈറ്റ്"), + ("Follow System", "സിസ്റ്റം അനുസരിച്ച്"), + ("Enable hardware codec", "ഹാർഡ്‌വെയർ കോഡെക് അനുവദിക്കുക"), + ("Unlock Security Settings", "സുരക്ഷാ ക്രമീകരണങ്ങൾ അൺലോക്ക് ചെയ്യുക"), + ("Enable audio", "ശബ്ദം അനുവദിക്കുക"), + ("Unlock Network Settings", "നെറ്റ്‌വർക്ക് ക്രമീകരണങ്ങൾ അൺലോക്ക് ചെയ്യുക"), + ("Server", "സെർവർ"), + ("Direct IP Access", "നേരിട്ടുള്ള IP ആക്‌സസ്"), + ("Proxy", "പ്രോക്സി"), + ("Apply", "പ്രയോഗിക്കുക"), + ("Disconnect all devices?", "എല്ലാ ഉപകരണങ്ങളും വിച്ഛേദിക്കണോ?"), + ("Clear", "വൃത്തിയാക്കുക"), + ("Audio Input Device", "ശബ്ദ ഇൻപുട്ട് ഉപകരണം"), + ("Use IP Whitelisting", "IP വൈറ്റ്‌ലിസ്റ്റിംഗ് ഉപയോഗിക്കുക"), + ("Network", "നെറ്റ്‌വർക്ക്"), + ("Pin Toolbar", "ടൂൾബാർ പിൻ ചെയ്യുക"), + ("Unpin Toolbar", "ടൂൾബാർ അൺപിൻ ചെയ്യുക"), + ("Recording", "റെക്കോർഡിംഗ്"), + ("Directory", "ഡയറക്ടറി"), + ("Automatically record incoming sessions", "വരുന്ന സെഷനുകൾ താനേ റെക്കോർഡ് ചെയ്യുക"), + ("Automatically record outgoing sessions", "പോകുന്ന സെഷനുകൾ താനേ റെക്കോർഡ് ചെയ്യുക"), + ("Change", "മാറ്റുക"), + ("Start session recording", "റെക്കോർഡിംഗ് തുടങ്ങുക"), + ("Stop session recording", "റെക്കോർഡിംഗ് നിർത്തുക"), + ("Enable recording session", "സെഷൻ റെക്കോർഡിംഗ് അനുവദിക്കുക"), + ("Enable LAN discovery", "LAN കണ്ടെത്തൽ അനുവദിക്കുക"), + ("Deny LAN discovery", "LAN കണ്ടെത്തൽ നിരസിക്കുക"), + ("Write a message", "സന്ദേശം എഴുതുക"), + ("Prompt", "പ്രോംപ്റ്റ്"), + ("Please wait for confirmation of UAC...", "UAC സ്ഥിരീകരണത്തിനായി കാത്തിരിക്കുക..."), + ("elevated_foreground_window_tip", "റിമോട്ടിലെ വിൻഡോയ്ക്ക് കൂടുതൽ അനുമതി ആവശ്യമാണ്."), + ("Disconnected", "വിച്ഛേദിച്ചു"), + ("Other", "മറ്റുള്ളവ"), + ("Confirm before closing multiple tabs", "ടാബുകൾ അടയ്ക്കുന്നതിന് മുൻപ് സ്ഥിരീകരിക്കുക"), + ("Keyboard Settings", "കീബോർഡ് ക്രമീകരണങ്ങൾ"), + ("Full Access", "പൂർണ്ണ ആക്‌സസ്"), + ("Screen Share", "സ്ക്രീൻ ഷെയർ"), + ("ubuntu-21-04-required", "Ubuntu 21.04 എങ്കിലും വേണം"), + ("wayland-requires-higher-linux-version", "Wayland-ന് പുതിയ ലിനക്സ് പതിപ്പ് ആവശ്യമാണ്"), + ("xdp-portal-unavailable", "XDP പോർട്ടൽ ലഭ്യമല്ല"), + ("JumpLink", "ജമ്പ്‌ലിങ്ക്"), + ("Please Select the screen to be shared(Operate on the peer side).", "പങ്കിടാനുള്ള സ്ക്രീൻ തിരഞ്ഞെടുക്കുക (മറുഭാഗത്ത് ചെയ്യുക)."), + ("Show RustDesk", "RustDesk കാണിക്കുക"), + ("This PC", "ഈ പിസി"), + ("or", "അല്ലെങ്കിൽ"), + ("Elevate", "എലിവേറ്റ് ചെയ്യുക"), + ("Zoom cursor", "സൂം കർസർ"), + ("Accept sessions via password", "പാസ്‌വേഡ് വഴി സെഷനുകൾ അനുവദിക്കുക"), + ("Accept sessions via click", "ക്ലിക്ക് വഴി സെഷനുകൾ അനുവദിക്കുക"), + ("Accept sessions via both", "രണ്ടും വഴി സെഷനുകൾ അനുവദിക്കുക"), + ("Please wait for the remote side to accept your session request...", "മറുഭാഗം അനുമതി നൽകാനായി കാത്തിരിക്കുക..."), + ("One-time Password", "ഒറ്റത്തവണ പാസ്‌വേഡ്"), + ("Use one-time password", "ഒറ്റത്തവണ പാസ്‌വേഡ് ഉപയോഗിക്കുക"), + ("One-time password length", "ഒറ്റത്തവണ പാസ്‌വേഡ് നീളം"), + ("Request access to your device", "നിങ്ങളുടെ ഉപകരണം ആക്‌സസ് ചെയ്യാൻ അനുമതി ചോദിക്കുന്നു"), + ("Hide connection management window", "കണക്ഷൻ മാനേജ്‌മെന്റ് വിൻഡോ മറയ്ക്കുക"), + ("hide_cm_tip", "പാസ്‌വേഡ് വഴിയുള്ള കണക്ഷൻ ആണെങ്കിൽ മാത്രം മറയ്ക്കുക"), + ("wayland_experiment_tip", "Wayland പിന്തുണ പരീക്ഷണാടിസ്ഥാനത്തിലാണ്"), + ("Right click to select tabs", "ടാബുകൾ തിരഞ്ഞെടുക്കാൻ വലത് ക്ലിക്ക് ചെയ്യുക"), + ("Skipped", "ഒഴിവാക്കി"), + ("Add to address book", "അഡ്രസ് ബുക്കിലേക്ക് ചേർക്കുക"), + ("Group", "ഗ്രൂപ്പ്"), + ("Search", "തിരയുക"), + ("Closed manually by web console", "വെബ് കൺസോൾ വഴി മാനുവലായി അടച്ചു"), + ("Local keyboard type", "ലോക്കൽ കീബോർഡ് തരം"), + ("Select local keyboard type", "ലോക്കൽ കീബോർഡ് തരം തിരഞ്ഞെടുക്കുക"), + ("software_render_tip", "സ്ക്രീൻ കറുത്തിരിക്കുകയാണെങ്കിൽ ഇത് പരീക്ഷിക്കുക"), + ("Always use software rendering", "എപ്പോഴും സോഫ്റ്റ്‌വെയർ റെൻഡറിംഗ് ഉപയോഗിക്കുക"), + ("config_input", "ഇൻപുട്ട് ക്രമീകരിക്കുക"), + ("config_microphone", "മൈക്രോഫോൺ ക്രമീകരിക്കുക"), + ("request_elevation_tip", "മറുഭാഗത്തുനിന്ന് എലവേഷൻ ആവശ്യപ്പെടുക"), + ("Wait", "കാത്തിരിക്കുക"), + ("Elevation Error", "എലവേഷൻ പിശക്"), + ("Ask the remote user for authentication", "റിമോട്ട് ഉപയോക്താവിനോട് അനുമതി ചോദിക്കുക"), + ("Choose this if the remote account is administrator", "റിമോട്ട് അക്കൗണ്ട് അഡ്മിനിസ്ട്രേറ്റർ ആണെങ്കിൽ ഇത് തിരഞ്ഞെടുക്കുക"), + ("Transmit the username and password of administrator", "അഡ്മിനിസ്ട്രേറ്റർ വിവരങ്ങൾ അയക്കുക"), + ("still_click_uac_tip", "റിമോട്ട് ഉപയോക്താവ് UAC വിൻഡോയിൽ 'അതെ' എന്ന് ക്ലിക്ക് ചെയ്യേണ്ടതുണ്ട്."), + ("Request Elevation", "എലവേഷൻ ആവശ്യപ്പെടുക"), + ("wait_accept_uac_tip", "റിമോട്ട് ഉപയോക്താവ് UAC അംഗീകരിക്കാൻ കാത്തിരിക്കുക."), + ("Elevate successfully", "വിജയകരമായി എലവേറ്റ് ചെയ്തു"), + ("uppercase", "വലിയ അക്ഷരം (Uppercase)"), + ("lowercase", "ചെറിയ അക്ഷരം (Lowercase)"), + ("digit", "അക്കം"), + ("special character", "പ്രത്യേക ചിഹ്നം"), + ("length>=8", "നീളം >= 8"), + ("Weak", "ദുർബലം"), + ("Medium", "ഇടത്തരം"), + ("Strong", "ശക്തം"), + ("Switch Sides", "വശങ്ങൾ മാറ്റുക"), + ("Please confirm if you want to share your desktop?", "നിങ്ങളുടെ ഡെസ്ക്ടോപ്പ് പങ്കിടണമെന്ന് നിങ്ങൾക്ക് ഉറപ്പാണോ?"), + ("Display", "ഡിസ്‌പ്ലേ"), + ("Default View Style", "സാധാരണ വ്യൂ സ്റ്റൈൽ"), + ("Default Scroll Style", "സാധാരണ സ്ക്രോൾ സ്റ്റൈൽ"), + ("Default Image Quality", "സാധാരണ ഇമേജ് ക്വാളിറ്റി"), + ("Default Codec", "സാധാരണ കോഡെക്"), + ("Bitrate", "ബിറ്റ്റേറ്റ്"), + ("FPS", "FPS"), + ("Auto", "ഓട്ടോ"), + ("Other Default Options", "മറ്റ് സാധാരണ ഓപ്ഷനുകൾ"), + ("Voice call", "വോയിസ് കോൾ"), + ("Text chat", "ടെക്സ്റ്റ് ചാറ്റ്"), + ("Stop voice call", "വോയിസ് കോൾ നിർത്തുക"), + ("relay_hint_tip", "നേരിട്ടുള്ള കണക്ഷൻ സാധ്യമല്ല; റിലേ വഴി ശ്രമിക്കാം."), + ("Reconnect", "വീണ്ടും കണക്ട് ചെയ്യുക"), + ("Codec", "കോഡെക്"), + ("Resolution", "റെസല്യൂഷൻ"), + ("No transfers in progress", "കൈമാറ്റങ്ങളൊന്നും നടക്കുന്നില്ല"), + ("Set one-time password length", "ഒറ്റത്തവണ പാസ്‌വേഡ് നീളം നിശ്ചയിക്കുക"), + ("RDP Settings", "RDP ക്രമീകരണങ്ങൾ"), + ("Sort by", "ക്രമീകരിക്കുക"), + ("New Connection", "പുതിയ കണക്ഷൻ"), + ("Restore", "പുനഃസ്ഥാപിക്കുക"), + ("Minimize", "ചുരുക്കുക"), + ("Maximize", "വലുതാക്കുക"), + ("Your Device", "നിങ്ങളുടെ ഉപകരണം"), + ("empty_recent_tip", "സമീപകാല സെഷനുകൾ ഇവിടെ കാണാം."), + ("empty_favorite_tip", "പ്രിയപ്പെട്ടവ ഇവിടെ കാണാം."), + ("empty_lan_tip", "ലോക്കൽ നെറ്റ്‌വർക്കിലെ ഉപകരണങ്ങൾ ഇവിടെ കാണാം."), + ("empty_address_book_tip", "അഡ്രസ് ബുക്ക് ശൂന്യമാണ്."), + ("Empty Username", "യൂസർ നെയിം നൽകിയില്ല"), + ("Empty Password", "പാസ്‌വേഡ് നൽകിയില്ല"), + ("Me", "ഞാൻ"), + ("identical_file_tip", "ഈ ഫയൽ നിലവിലുണ്ട്."), + ("show_monitors_tip", "ടൂൾബാറിൽ മോണിറ്ററുകൾ കാണിക്കുക"), + ("View Mode", "വ്യൂ മോഡ്"), + ("login_linux_tip", "റിമോട്ട് ലിനക്സ് സെഷനായി ലോഗിൻ ചെയ്യണം"), + ("verify_rustdesk_password_tip", "RustDesk പാസ്‌വേഡ് പരിശോധിക്കുക"), + ("remember_account_tip", "ഈ അക്കൗണ്ട് ഓർമ്മിക്കുക"), + ("os_account_desk_tip", "ആക്‌സസിനായി OS അക്കൗണ്ട് ഉപയോഗിക്കുക"), + ("OS Account", "OS അക്കൗണ്ട്"), + ("another_user_login_title_tip", "മറ്റൊരു ഉപയോക്താവ് ലോഗിൻ ചെയ്തിട്ടുണ്ട്"), + ("another_user_login_text_tip", "വിച്ഛേദിച്ച ശേഷം വീണ്ടും ശ്രമിക്കുക"), + ("xorg_not_found_title_tip", "Xorg കണ്ടെത്താനായില്ല"), + ("xorg_not_found_text_tip", "ദയവായി Xorg ഇൻസ്റ്റാൾ ചെയ്യുക"), + ("no_desktop_title_tip", "ഡെസ്ക്ടോപ്പ് ലഭ്യമല്ല"), + ("no_desktop_text_tip", "ദയവായി ലിനക്സ് ഡെസ്ക്ടോപ്പ് ഇൻസ്റ്റാൾ ചെയ്യുക"), + ("No need to elevate", "എലവേറ്റ് ചെയ്യേണ്ടതില്ല"), + ("System Sound", "സിസ്റ്റം സൗണ്ട്"), + ("Default", "ഡിഫോൾട്ട്"), + ("New RDP", "പുതിയ RDP"), + ("Fingerprint", "ഫിംഗർപ്രിന്റ്"), + ("Copy Fingerprint", "ഫിംഗർപ്രിന്റ് കോപ്പി ചെയ്യുക"), + ("no fingerprints", "ഫിംഗർപ്രിന്റുകൾ ഇല്ല"), + ("Select a peer", "ഒരാളെ തിരഞ്ഞെടുക്കുക"), + ("Select peers", "തിരഞ്ഞെടുക്കുക"), + ("Plugins", "പ്ലഗിനുകൾ"), + ("Uninstall", "അൺഇൻസ്റ്റാൾ ചെയ്യുക"), + ("Update", "അപ്ഡേറ്റ് ചെയ്യുക"), + ("Enable", "പ്രവർത്തനക്ഷമമാക്കുക"), + ("Disable", "പ്രവർത്തനരഹിതമാക്കുക"), + ("Options", "ഓപ്ഷനുകൾ"), + ("resolution_original_tip", "ഒറിജിനൽ റെസല്യൂഷൻ"), + ("resolution_fit_local_tip", "ലോക്കൽ സ്ക്രീനിന് അനുയോജ്യം"), + ("resolution_custom_tip", "കസ്റ്റം റെസല്യൂഷൻ"), + ("Collapse toolbar", "ടൂൾബാർ ചുരുക്കുക"), + ("Accept and Elevate", "അംഗീകരിച്ച് എലവേറ്റ് ചെയ്യുക"), + ("accept_and_elevate_btn_tooltip", "കണക്ഷൻ അംഗീകരിച്ച് UAC അനുമതികൾ നൽകുക."), + ("clipboard_wait_response_timeout_tip", "ക്ലിപ്പ്ബോർഡ് മറുപടിക്കായി കാത്തിരുന്നു സമയം കഴിഞ്ഞു."), + ("Incoming connection", "വരുന്ന കണക്ഷൻ"), + ("Outgoing connection", "പോകുന്ന കണക്ഷൻ"), + ("Exit", "പുറത്തുകടക്കുക"), + ("Open", "തുറക്കുക"), + ("logout_tip", "നിങ്ങൾക്ക് ലോഗൗട്ട് ചെയ്യണമെന്ന് ഉറപ്പാണോ?"), + ("Service", "സർവീസ്"), + ("Start", "തുടങ്ങുക"), + ("Stop", "നിർത്തുക"), + ("exceed_max_devices", "നിങ്ങൾ ഉപകരണങ്ങളുടെ പരിധി കവിഞ്ഞു."), + ("Sync with recent sessions", "സമീപകാല സെഷനുകളുമായി സિંക് ചെയ്യുക"), + ("Sort tags", "ടാഗുകൾ ക്രമീകരിക്കുക"), + ("Open connection in new tab", "പുതിയ ടാബിൽ തുറക്കുക"), + ("Move tab to new window", "ടാബ് പുതിയ വിൻഡോയിലേക്ക് മാറ്റുക"), + ("Can not be empty", "ശൂന്യമാകാൻ പാടില്ല"), + ("Already exists", "നിലവിലുണ്ട്"), + ("Change Password", "പാസ്‌വേഡ് മാറ്റുക"), + ("Refresh Password", "പാസ്‌വേഡ് പുതുക്കുക"), + ("ID", "ഐഡി"), + ("Grid View", "ഗ്രിഡ് വ്യൂ"), + ("List View", "ലിസ്റ്റ് വ്യൂ"), + ("Select", "തിരഞ്ഞെടുക്കുക"), + ("Toggle Tags", "ടാഗുകൾ മാറ്റുക"), + ("pull_ab_failed_tip", "അഡ്രസ് ബുക്ക് അപ്‌ഡേറ്റ് ചെയ്യുന്നതിൽ പരാജയപ്പെട്ടു."), + ("push_ab_failed_tip", "അഡ്രസ് ബുക്ക് സિંക് ചെയ്യുന്നതിൽ പരാജയപ്പെട്ടു."), + ("synced_peer_readded_tip", "സമീപകാല ഉപകരണം അഡ്രസ് ബുക്കിലേക്ക് സિંക് ചെയ്തു."), + ("Change Color", "നിറം മാറ്റുക"), + ("Primary Color", "പ്രധാന നിറം"), + ("HSV Color", "HSV നിറം"), + ("Installation Successful!", "ഇൻസ്റ്റാളേഷൻ വിജയിച്ചു!"), + ("Installation failed!", "ഇൻസ്റ്റാളേഷൻ പരാജയപ്പെട്ടു!"), + ("Reverse mouse wheel", "മൗസ് വീൽ തിരിക്കുക"), + ("{} sessions", "{} സെഷനുകൾ"), + ("scam_title", "തട്ടിപ്പ് മുന്നറിയിപ്പ്!"), + ("scam_text1", "നിങ്ങൾക്ക് പരിചയമില്ലാത്ത ആരെങ്കിലും RustDesk ഉപയോഗിക്കാൻ ആവശ്യപ്പെട്ടാൽ ഉടൻ കണക്ഷൻ വിച്ഛേദിക്കുക."), + ("scam_text2", "ഇതൊരു തട്ടിപ്പായിരിക്കാം. ആർക്കും പാസ്‌വേഡ് നൽകരുത്."), + ("Don't show again", "വീണ്ടും കാണിക്കരുത്"), + ("I Agree", "ഞാൻ സമ്മതിക്കുന്നു"), + ("Decline", "നിരസിക്കുന്നു"), + ("Timeout in minutes", "മിനിറ്റുകളിൽ സമയം നിശ്ചയിക്കുക"), + ("auto_disconnect_option_tip", "പ്രവർത്തനമില്ലെങ്കിൽ താനേ വിച്ഛേദിക്കുക"), + ("Connection failed due to inactivity", "പ്രവർത്തനമില്ലാത്തതിനാൽ കണക്ഷൻ വിച്ഛേദിച്ചു"), + ("Check for software update on startup", "തുടങ്ങുമ്പോൾ അപ്‌ഡേറ്റ് ഉണ്ടോ എന്ന് പരിശോധിക്കുക"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "സെർവർ പ്രോ {} ലേക്ക് അപ്‌ഗ്രേഡ് ചെയ്യുക"), + ("pull_group_failed_tip", "ഗ്രൂപ്പ് വിവരങ്ങൾ ലഭിക്കുന്നതിൽ പരാജയപ്പെട്ടു"), + ("Filter by intersection", "ഇന്റർസെക്ഷൻ വഴി ഫിൽട്ടർ ചെയ്യുക"), + ("Remove wallpaper during incoming sessions", "കണക്ഷൻ സമയത്ത് വാൾപേപ്പർ മാറ്റുക"), + ("Test", "പരിശോധിക്കുക"), + ("display_is_plugged_out_msg", "ഡിസ്‌പ്ലേ ഊരിയിരിക്കുകയാണ്."), + ("No displays", "ഡിസ്‌പ്ലേകൾ ഇല്ല"), + ("Open in new window", "പുതിയ വിൻഡോയിൽ തുറക്കുക"), + ("Show displays as individual windows", "ഓരോ ഡിസ്‌പ്ലേയും ഓരോ വിൻഡോയായി കാണിക്കുക"), + ("Use all my displays for the remote session", "എല്ലാ ഡിസ്‌പ്ലേകളും ഉപയോഗിക്കുക"), + ("selinux_tip", "SELinux പ്രവർത്തനക്ഷമമാണ്."), + ("Change view", "കാഴ്ച മാറ്റുക"), + ("Big tiles", "വലിയ ടൈലുകൾ"), + ("Small tiles", "ചെറിയ ടൈലുകൾ"), + ("List", "ലിസ്റ്റ്"), + ("Virtual display", "വെർച്വൽ ഡിസ്‌പ്ലേ"), + ("Plug out all", "എല്ലാം ഊരുക"), + ("True color (4:4:4)", "ട്രൂ കളർ (4:4:4)"), + ("Enable blocking user input", "യൂസർ ഇൻപുട്ട് തടയുന്നത് അനുവദിക്കുക"), + ("id_input_tip", "നിങ്ങൾക്ക് ഐഡി, ഏലിയാസ് അല്ലെങ്കിൽ ഐപി നൽകാം."), + ("privacy_mode_impl_mag_tip", "മാഗ്നിഫയർ സ്വകാര്യ മോഡ്"), + ("privacy_mode_impl_virtual_display_tip", "വെർച്വൽ ഡിസ്‌പ്ലേ സ്വകാര്യ മോഡ്"), + ("Enter privacy mode", "സ്വകാര്യ മോഡിലേക്ക് കടക്കുക"), + ("Exit privacy mode", "സ്വകാര്യ മോഡിൽ നിന്ന് പുറത്തുകടക്കുക"), + ("idd_not_support_under_win10_2004_tip", "Windows 10 (2004) എങ്കിലും വേണം."), + ("input_source_1_tip", "ഇൻപുട്ട് സോഴ്സ് 1"), + ("input_source_2_tip", "ഇൻപുട്ട് സോഴ്സ് 2"), + ("Swap control-command key", "Control-Command കീകൾ പരസ്പരം മാറ്റുക"), + ("swap-left-right-mouse", "ഇടത്-വലത് മൗസ് ബട്ടണുകൾ മാറ്റുക"), + ("2FA code", "2FA കോഡ്"), + ("More", "കൂടുതൽ"), + ("enable-2fa-title", "2FA ഓൺ ചെയ്യുക"), + ("enable-2fa-desc", "അതന്റിക്കേറ്റർ ആപ്പ് സജ്ജമാക്കുക."), + ("wrong-2fa-code", "തെറ്റായ 2FA കോഡ്."), + ("enter-2fa-title", "2FA കോഡ് നൽകുക"), + ("Email verification code must be 6 characters.", "ഇമെയിൽ കോഡ് 6 അക്ഷരങ്ങൾ വേണം."), + ("2FA code must be 6 digits.", "2FA കോഡ് 6 അക്കങ്ങൾ വേണം."), + ("Multiple Windows sessions found", "ഒന്നിലധികം വിൻഡോസ് സെഷനുകൾ കണ്ടെത്തി"), + ("Please select the session you want to connect to", "ബന്ധിപ്പിക്കേണ്ട സെഷൻ തിരഞ്ഞെടുക്കുക"), + ("powered_by_me", "ഞാൻ നിർമ്മിച്ചത്"), + ("outgoing_only_desk_tip", "ഇതൊരു ഔട്ട്‌ഗോയിംഗ് മോഡ് മാത്രമാണ്"), + ("preset_password_warning", "സുരക്ഷയ്ക്കായി പാസ്‌വേഡ് മാറ്റുക."), + ("Security Alert", "സുരക്ഷാ മുന്നറിയിപ്പ്"), + ("My address book", "എന്റെ അഡ്രസ് ബുക്ക്"), + ("Personal", "വ്യക്തിഗതം"), + ("Owner", "ഉടമസ്ഥൻ"), + ("Set shared password", "പങ്കിട്ട പാസ്‌വേഡ് സജ്ജമാക്കുക"), + ("Exist in", "നിലവിലുള്ളത്"), + ("Read-only", "വായിക്കാൻ മാത്രം"), + ("Read/Write", "വായിക്കാനും എഴുതാനും"), + ("Full Control", "പൂർണ്ണ നിയന്ത്രണം"), + ("share_warning_tip", "നിങ്ങളുടെ വിവരങ്ങൾ പങ്കിടുകയാണ്."), + ("Everyone", "എല്ലാവരും"), + ("ab_web_console_tip", "വെബ് കൺസോൾ അഡ്രസ് ബുക്ക്"), + ("allow-only-conn-window-open-tip", "RustDesk വിൻഡോ തുറന്നിരിക്കുമ്പോൾ മാത്രം കണക്ഷൻ അനുവദിക്കുക"), + ("no_need_privacy_mode_no_physical_displays_tip", "ഡിസ്‌പ്ലേ ഇല്ലാത്തതിനാൽ സ്വകാര്യ മോഡ് ആവശ്യമില്ല."), + ("Follow remote cursor", "റിമോട്ട് കർസറിനെ പിന്തുടരുക"), + ("Follow remote window focus", "റിമോട്ട് വിൻഡോ ഫോക്കസിനെ പിന്തുടരുക"), + ("default_proxy_tip", "ഡിഫോൾട്ട് പ്രോക്സി ക്രമീകരണം"), + ("no_audio_input_device_tip", "ഓഡിയോ ഇൻപുട്ട് ഉപകരണം കണ്ടെത്തിയില്ല."), + ("Incoming", "വരുന്നവ"), + ("Outgoing", "പോകുന്നവ"), + ("Clear Wayland screen selection", "Wayland സ്ക്രീൻ സെലക്ഷൻ മാറ്റുക"), + ("clear_Wayland_screen_selection_tip", "സ്ക്രീൻ സെലക്ഷൻ റീസെറ്റ് ചെയ്യുക."), + ("confirm_clear_Wayland_screen_selection_tip", "സെലക്ഷൻ മാറ്റണമെന്ന് ഉറപ്പാണോ?"), + ("android_new_voice_call_tip", "പുതിയ വോയിസ് കോൾ അഭ്യർത്ഥന"), + ("texture_render_tip", "ടെക്സ്ചർ റെൻഡറിംഗ് ഉപയോഗിക്കുക"), + ("Use texture rendering", "ടെക്സ്ചർ റെൻഡറിംഗ് ഉപയോഗിക്കുക"), + ("Floating window", "ഫ്ലോട്ടിംഗ് വിൻഡോ"), + ("floating_window_tip", "ബാക്ക്ഗ്രൗണ്ടിലാണെങ്കിലും RustDesk കാണിക്കുക"), + ("Keep screen on", "സ്ക്രീൻ ഓഫ് ആകാതെ വെക്കുക"), + ("Never", "ഒരിക്കലുമില്ല"), + ("During controlled", "നിയന്ത്രിക്കുമ്പോൾ"), + ("During service is on", "സർവീസ് ഓൺ ആയിരിക്കുമ്പോൾ"), + ("Capture screen using DirectX", "DirectX ഉപയോഗിച്ച് സ്ക്രീൻ ക്യാപ്ചർ ചെയ്യുക"), + ("Back", "പുറകോട്ട്"), + ("Apps", "ആപ്പുകൾ"), + ("Volume up", "ശബ്ദം കൂട്ടുക"), + ("Volume down", "ശബ്ദം കുറയ്ക്കുക"), + ("Power", "പവർ"), + ("Telegram bot", "ടെലഗ്രാം ബോട്ട്"), + ("enable-bot-tip", "അറിയിപ്പുകൾക്കായി ബോട്ട് ഓൺ ചെയ്യുക"), + ("enable-bot-desc", "ടെലഗ്രാം ബോട്ട് സജ്ജമാക്കുക."), + ("cancel-2fa-confirm-tip", "2FA റദ്ദാക്കണമെന്ന് ഉറപ്പാണോ?"), + ("cancel-bot-confirm-tip", "ബോട്ട് റദ്ദാക്കണമെന്ന് ഉറപ്പാണോ?"), + ("About RustDesk", "RustDesk-നെ കുറിച്ച്"), + ("Send clipboard keystrokes", "ക്ലിപ്പ്ബോർഡ് കീസ്ട്രോക്കുകൾ അയക്കുക"), + ("network_error_tip", "നെറ്റ്‌വർക്ക് പിശക്, വീണ്ടും ശ്രമിക്കുക."), + ("Unlock with PIN", "പിൻ ഉപയോഗിച്ച് അൺലോക്ക് ചെയ്യുക"), + ("Requires at least {} characters", "കുറഞ്ഞത് {} അക്ഷരങ്ങൾ വേണം"), + ("Wrong PIN", "തെറ്റായ പിൻ"), + ("Set PIN", "പിൻ സജ്ജമാക്കുക"), + ("Enable trusted devices", "വിശ്വസനീയമായ ഉപകരണങ്ങൾ അനുവദിക്കുക"), + ("Manage trusted devices", "വിശ്വസനീയമായ ഉപകരണങ്ങൾ നിയന്ത്രിക്കുക"), + ("Platform", "പ്ലാറ്റ്‌ഫോം"), + ("Days remaining", "ബാക്കിയുള്ള ദിവസങ്ങൾ"), + ("enable-trusted-devices-tip", "വിശ്വസനീയമായവയ്ക്ക് പാസ്‌വേഡ് വേണ്ട"), + ("Parent directory", "പ്രധാന ഡയറക്ടറി"), + ("Resume", "തുടരുക"), + ("Invalid file name", "അസാധുവായ ഫയൽ പേര്"), + ("one-way-file-transfer-tip", "ഒരു വശത്തേക്ക് മാത്രമുള്ള ഫയൽ കൈമാറ്റം"), + ("Authentication Required", "അംഗീകാരം ആവശ്യമാണ്"), + ("Authenticate", "അംഗീകരിക്കുക"), + ("web_id_input_tip", "റിമോട്ട് ഐഡി നൽകുക"), + ("Download", "ഡൗൺലോഡ്"), + ("Upload folder", "ഫോൾഡർ അപ്‌ലോഡ് ചെയ്യുക"), + ("Upload files", "ഫയലുകൾ അപ്‌ലോഡ് ചെയ്യുക"), + ("Clipboard is synchronized", "ക്ലിപ്പ്ബോർഡ് സങ്കലനം ചെയ്തു"), + ("Update client clipboard", "ക്ലയന്റ് ക്ലിപ്പ്ബോർഡ് പുതുക്കുക"), + ("Untagged", "ടാഗ് ചെയ്യാത്തവ"), + ("new-version-of-{}-tip", "{} പുതിയ പതിപ്പ് ലഭ്യമാണ്"), + ("Accessible devices", "ലഭ്യമായ ഉപകരണങ്ങൾ"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "റിമോട്ട് പതിപ്പ് {} ലേക്ക് മാറ്റുക"), + ("d3d_render_tip", "D3D റെൻഡറിംഗ് ഉപയോഗിക്കുക"), + ("Printer", "പ്രിന്റർ"), + ("printer-os-requirement-tip", "പ്രിന്റിംഗിന് വിൻഡോസ് വേണം."), + ("printer-requires-installed-{}-client-tip", "ഇതിന് {} ക്ലയന്റ് ഇൻസ്റ്റാൾ ചെയ്യണം."), + ("printer-{}-not-installed-tip", "പ്രിന്റർ {} ഇൻസ്റ്റാൾ ചെയ്തിട്ടില്ല."), + ("printer-{}-ready-tip", "പ്രിന്റർ {} തയ്യാറാണ്."), + ("Install {} Printer", "{} പ്രിന്റർ ഇൻസ്റ്റാൾ ചെയ്യുക"), + ("Outgoing Print Jobs", "പോകുന്ന പ്രിന്റ് ജോലികൾ"), + ("Incoming Print Jobs", "വരുന്ന പ്രിന്റ് ജോലികൾ"), + ("Incoming Print Job", "വരുന്ന പ്രിന്റ് ജോലി"), + ("use-the-default-printer-tip", "ഡിഫോൾട്ട് പ്രിന്റർ ഉപയോഗിക്കുക"), + ("use-the-selected-printer-tip", "തിഞ്ഞെടുത്ത പ്രിന്റർ ഉപയോഗിക്കുക"), + ("auto-print-tip", "താനേ പ്രിന്റ് ചെയ്യുക"), + ("print-incoming-job-confirm-tip", "പ്രിന്റ് ചെയ്യുന്നതിന് മുൻപ് ചോദിക്കുക"), + ("remote-printing-disallowed-tile-tip", "റിമോട്ട് പ്രിന്റിംഗ് അനുവദനീയമല്ല"), + ("remote-printing-disallowed-text-tip", "സെറ്റിംഗ്സിൽ റിമോട്ട് പ്രിന്റിംഗ് ഓൺ ചെയ്യുക."), + ("save-settings-tip", "സെറ്റിംഗ്സ് സേവ് ചെയ്യുക"), + ("dont-show-again-tip", "വീണ്ടും കാണിക്കരുത്"), + ("Take screenshot", "സ്ക്രീൻഷോട്ട് എടുക്കുക"), + ("Taking screenshot", "സ്ക്രീൻഷോട്ട് എടുക്കുന്നു"), + ("screenshot-merged-screen-not-supported-tip", "മെർജ് ചെയ്ത സ്ക്രീൻഷോട്ട് പിന്തുണയ്ക്കുന്നില്ല."), + ("screenshot-action-tip", "സ്ക്രീൻഷോട്ടിന് ശേഷമുള്ള നടപടി"), + ("Save as", "പേരിൽ സേവ് ചെയ്യുക"), + ("Copy to clipboard", "ക്ലിപ്പ്ബോർഡിലേക്ക് കോപ്പി ചെയ്യുക"), + ("Enable remote printer", "റിമോട്ട് പ്രിന്റർ അനുവദിക്കുക"), + ("Downloading {}", "{} ഡൗൺലോഡ് ചെയ്യുന്നു"), + ("{} Update", "{} അപ്‌ഡേറ്റ്"), + ("{}-to-update-tip", "അപ്‌ഡേറ്റ് ചെയ്യാൻ {}"), + ("download-new-version-failed-tip", "പുതിയ പതിപ്പ് ഡൗൺലോഡ് ചെയ്യുന്നതിൽ പരാജയപ്പെട്ടു."), + ("Auto update", "ഓട്ടോ അപ്‌ഡേറ്റ്"), + ("update-failed-check-msi-tip", "അപ്‌ഡേറ്റ് പരാജയപ്പെട്ടു, MSI ഫയൽ പരിശോധിക്കുക."), + ("websocket_tip", "പോട്ടുകൾ തടഞ്ഞിട്ടുണ്ടെങ്കിൽ WebSocket ഉപയോഗിക്കുക."), + ("Use WebSocket", "WebSocket ഉപയോഗിക്കുക"), + ("Trackpad speed", "ട്രാക്ക്പാഡ് വേഗത"), + ("Default trackpad speed", "സാധാരണ ട്രാക്ക്പാഡ് വേഗത"), + ("Numeric one-time password", "അക്കങ്ങൾ മാത്രമുള്ള OTP"), + ("Enable IPv6 P2P connection", "IPv6 P2P കണക്ഷൻ അനുവദിക്കുക"), + ("Enable UDP hole punching", "UDP ഹോൾ പഞ്ചിംഗ് അനുവദിക്കുക"), + ("View camera", "ക്യാമറ കാണുക"), + ("Enable camera", "ക്യാമറ ഓൺ ചെയ്യുക"), + ("No cameras", "ക്യാമറകൾ കണ്ടെത്തിയില്ല"), + ("view_camera_unsupported_tip", "റിമോട്ട് ക്യാമറ പിന്തുണയ്ക്കുന്നില്ല."), + ("Terminal", "ടെർമിനൽ"), + ("Enable terminal", "ടെർമിനൽ അനുവദിക്കുക"), + ("New tab", "പുതിയ ടാബ്"), + ("Keep terminal sessions on disconnect", "വിച്ഛേദിക്കുമ്പോൾ ടെർമിനൽ സെഷൻ നിർത്തരുത്"), + ("Terminal (Run as administrator)", "ടെർമിനൽ (അഡ്മിനിസ്ട്രേറ്ററായി)"), + ("terminal-admin-login-tip", "അഡ്മിൻ ലോഗിൻ ആവശ്യമാണ്."), + ("Failed to get user token.", "യൂസർ ടോക്കൺ ലഭിക്കുന്നതിൽ പരാജയപ്പെട്ടു."), + ("Incorrect username or password.", "തെറ്റായ യൂസർ നെയിം അല്ലെങ്കിൽ പാസ്‌വേഡ്."), + ("The user is not an administrator.", "ഉപയോക്താവ് അഡ്മിനിസ്ട്രേറ്ററല്ല."), + ("Failed to check if the user is an administrator.", "അഡ്മിൻ ആണോ എന്ന് പരിശോധിക്കുന്നതിൽ പരാജയപ്പെട്ടു."), + ("Supported only in the installed version.", "ഇൻസ്റ്റാൾ ചെയ്ത പതിപ്പിൽ മാത്രം ലഭ്യം."), + ("elevation_username_tip", "അഡ്മിനിസ്ട്രേറ്റർ പേര് നൽകുക"), + ("Preparing for installation ...", "ഇൻസ്റ്റാളേഷനായി ഒരുങ്ങുന്നു..."), + ("Show my cursor", "എന്റെ കർസർ കാണിക്കുക"), + ("Scale custom", "കസ്റ്റം സ്കെയിൽ"), + ("Custom scale slider", "കസ്റ്റം സ്കെയിൽ സ്ലൈഡർ"), + ("Decrease", "കുറയ്ക്കുക"), + ("Increase", "കൂട്ടുക"), + ("Show virtual mouse", "വെർച്വൽ മൗസ് കാണിക്കുക"), + ("Virtual mouse size", "വെർച്വൽ മൗസ് വലിപ്പം"), + ("Small", "ചെറുത്"), + ("Large", "വലുത്"), + ("Show virtual joystick", "വെർച്വൽ ജോയ്സ്റ്റിക് കാണിക്കുക"), + ("Edit note", "കുറിപ്പ് മാറ്റുക"), + ("Alias", "ഏലിയാസ് (Alias)"), + ("ScrollEdge", "സ്ക്രോൾ എഡ്ജ്"), + ("Allow insecure TLS fallback", "സുരക്ഷിതമല്ലാത്ത TLS അനുവദിക്കുക"), + ("allow-insecure-tls-fallback-tip", "പഴയ സെർവറുകൾക്കായി ഉപയോഗിക്കുക."), + ("Disable UDP", "UDP ഒഴിവാക്കുക"), + ("disable-udp-tip", "കണക്ഷൻ പ്രശ്നങ്ങൾക്ക് UDP ഒഴിവാക്കുക."), + ("server-oss-not-support-tip", "OSS സെർവർ ഇത് പിന്തുണയ്ക്കുന്നില്ല."), + ("input note here", "ഇവിടെ കുറിപ്പ് എഴുതുക"), + ("note-at-conn-end-tip", "കണക്ഷൻ കഴിയുമ്പോൾ കുറിപ്പ് കാണിക്കുക"), + ("Show terminal extra keys", "ടെർമിനൽ കീകൾ കാണിക്കുക"), + ("Relative mouse mode", "റിലേറ്റീവ് മൗസ് മോഡ്"), + ("rel-mouse-not-supported-peer-tip", "മറുഭാഗം പിന്തുണയ്ക്കുന്നില്ല."), + ("rel-mouse-not-ready-tip", "തയ്യാറായിട്ടില്ല."), + ("rel-mouse-lock-failed-tip", "മൗസ് ലോക്ക് പരാജയപ്പെട്ടു."), + ("rel-mouse-exit-{}-tip", "പുറത്തുകടക്കാൻ {} അമർത്തുക"), + ("rel-mouse-permission-lost-tip", "അനുമതി നഷ്ടപ്പെട്ടു."), + ("Changelog", "മാറ്റങ്ങൾ (Changelog)"), + ("keep-awake-during-outgoing-sessions-label", "സെഷൻ നടക്കുമ്പോൾ ഉറക്കത്തിലാകരുത്"), + ("keep-awake-during-incoming-sessions-label", "സെഷൻ വരുമ്പോൾ ഉറക്കത്തിലാകരുത്"), + ("Continue with {}", "{} ഉപയോഗിച്ച് തുടരുക"), + ("Display Name", "ഡിസ്‌പ്ലേ പേര്"), + ("password-hidden-tip", "സുരക്ഷയ്ക്കായി പാസ്‌വേഡ് മറച്ചിരിക്കുന്നു."), + ("preset-password-in-use-tip", "പ്രീസെറ്റ് പാസ്‌വേഡ് ഉപയോഗത്തിലാണ്."), + ].iter().cloned().collect(); +} From e0c5e1483ead437dd196338e52161a0ad76a0d21 Mon Sep 17 00:00:00 2001 From: "Re*Index. (ot_inc)" <32851879+reindex-ot@users.noreply.github.com> Date: Fri, 24 Apr 2026 00:52:21 +0900 Subject: [PATCH 228/277] Update Japanese translate (#14838) * Update ja.rs * Update ja.rs * Fix typo --- src/lang/ja.rs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 805898ef9..56faba383 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -661,9 +661,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("printer-{}-not-installed-tip", "{} のプリンターがインストールされていません。"), ("printer-{}-ready-tip", "{} のプリンターがインストールされ、使用可能になっています。"), ("Install {} Printer", " {} のプリンターをインストール"), - ("Outgoing Print Jobs", "送信印刷ジョブ"), - ("Incoming Print Jobs", "受信印刷ジョブ"), - ("Incoming Print Job", "受信印刷ジョブ"), + ("Outgoing Print Jobs", "印刷ジョブの送信"), + ("Incoming Print Jobs", "印刷ジョブの受信"), + ("Incoming Print Job", "印刷ジョブの受信"), ("use-the-default-printer-tip", "既定のプリンターを使用する"), ("use-the-selected-printer-tip", "選択したプリンターを使用する"), ("auto-print-tip", "選択したプリンターを使用して自動的に印刷する"), @@ -710,7 +710,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevation_username_tip", "ユーザー名またはドメインのユーザー名を入力してください。"), ("Preparing for installation ...", "インストールの準備中です..."), ("Show my cursor", "自分のカーソルを表示する"), - ("Scale custom", "カスタムスケーリング"), + ("Scale custom", "カスタムスケール"), ("Custom scale slider", "カスタムスケールのスライダー"), ("Decrease", "縮小"), ("Increase", "拡大"), @@ -730,18 +730,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("input note here", "ここにメモを入力"), ("note-at-conn-end-tip", "接続終了時にメモを要求する"), ("Show terminal extra keys", "ターミナルの追加キーを表示する"), - ("Relative mouse mode", ""), - ("rel-mouse-not-supported-peer-tip", ""), - ("rel-mouse-not-ready-tip", ""), - ("rel-mouse-lock-failed-tip", ""), - ("rel-mouse-exit-{}-tip", ""), - ("rel-mouse-permission-lost-tip", ""), - ("Changelog", ""), - ("keep-awake-during-outgoing-sessions-label", ""), - ("keep-awake-during-incoming-sessions-label", ""), - ("Continue with {}", "{} で続行"), - ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), + ("Relative mouse mode", "相対マウスモード"), + ("rel-mouse-not-supported-peer-tip", "接続先のデバイスは相対マウスモードに対応していません。"), + ("rel-mouse-not-ready-tip", "相対マウスモードはまだ準備できていません。再度お試しください。"), + ("rel-mouse-lock-failed-tip", "カーソルをロックできませんでした。相対マウスモードは無効化されています。"), + ("rel-mouse-exit-{}-tip", "「{}」を押して終了します。"), + ("rel-mouse-permission-lost-tip", "キーボード操作の権限が取り消されました。相対マウスモードは無効化されています。"), + ("Changelog", "更新履歴"), + ("keep-awake-during-outgoing-sessions-label", "送信セッション中は、画面のスリープを無効化する"), + ("keep-awake-during-incoming-sessions-label", "受信セッション中は、画面のスリープを無効化する"), + ("Continue with {}", "{}で続行する"), + ("Display Name", "表示名"), + ("password-hidden-tip", "永続的なパスワードが設定されています (非表示)"), + ("preset-password-in-use-tip", "プリセットパスワードが現在使用されています"), ].iter().cloned().collect(); } From 5d0533f0d4a86673982098b4b62d286f60f11cf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aliaksandr=20Kliuje=C5=AD?= Date: Thu, 23 Apr 2026 17:52:43 +0200 Subject: [PATCH 229/277] Update Balarusian strings (#14842) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update Balarusian strings * BE: fix typos * BE: fix ў-related typos --- src/lang/be.rs | 742 ++++++++++++++++++++++++------------------------- 1 file changed, 371 insertions(+), 371 deletions(-) diff --git a/src/lang/be.rs b/src/lang/be.rs index 6c6a13315..5ea7c3351 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -1,19 +1,19 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ - ("Status", "Статус"), + ("Status", "Стан"), ("Your Desktop", "Ваш працоўны стол"), ("desk_tip", "Ваш працоўны стол даступны з гэтым ID і паролем."), ("Password", "Пароль"), - ("Ready", "Гатовы"), + ("Ready", "Гатова"), ("Established", "Усталявана"), - ("connecting_status", "Падключэнне да сеткі RustDesk..."), + ("connecting_status", "Ідзе падключэнне да сеткі RustDesk..."), ("Enable service", "Уключыць службу"), ("Start service", "Запусціць службу"), ("Service is running", "Служба запушчана"), ("Service is not running", "Служба не запушчана"), - ("not_ready_status", "Не падключана. Праверце злучэнне."), - ("Control Remote Desktop", "Кіраванне выдаленым працоўным сталом"), + ("not_ready_status", "Не падключана. Праверце падключэнне."), + ("Control Remote Desktop", "Новае падключэнне"), ("Transfer file", "Перадаць файлы"), ("Connect", "Падключыцца"), ("Recent sessions", "Апошнія сеансы"), @@ -22,7 +22,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("TCP tunneling", "TCP-тунэляванне"), ("Remove", "Выдаліць"), ("Refresh random password", "Абнавіць выпадковы пароль"), - ("Set your own password", "Усталяваць свой пароль"), + ("Set your own password", "Задаць свой пароль"), ("Enable keyboard/mouse", "Выкарыстоўваць клавіятуру/мыш"), ("Enable clipboard", "Выкарыстоўваць буфер абмену"), ("Enable file transfer", "Выкарыстоўваць перадачу файлаў"), @@ -41,17 +41,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "даўжыня %min%...%max%"), ("starts with a letter", "пачынаецца з літары"), ("allowed characters", "дазволеныя сімвалы"), - ("id_change_tip", "Дапускаюцца толькі сімвалы a-z, A-Z, 0-9, - (dash) і _ (падкрэсліванне). Першай павінна быць літара a-z, A-Z. Даўжыня ад 6 да 16."), + ("id_change_tip", "Дазволена выкарыстоўваць толькі сімвалы a-z, A-Z, 0-9, - (dash) і _ (падкрэсліванне). Першай павінна быць літара a-z, A-Z. Даўжыня ад 6 да 16."), ("Website", "Сайт"), ("About", "Пра праграму"), ("Slogan_tip", "Зроблена з душой у гэтым вар'яцкім свеце!"), - ("Privacy Statement", "Заява аб канфідэнцыяльнасці"), + ("Privacy Statement", "Заява аб канфідэнцыйнасці"), ("Mute", "Адключыць гук"), ("Build Date", "Дата зборкі"), ("Version", "Версія"), ("Home", "Галоўная"), - ("Audio Input", "Аўдыёўваход"), - ("Enhancements", "Палепшанні"), + ("Audio Input", "Аўдыяўваход"), + ("Enhancements", "Паляпшэнні"), ("Hardware Codec", "Апаратны кодэк"), ("Adaptive bitrate", "Адаптыўны бітрэйт"), ("ID Server", "Сервер ID"), @@ -63,10 +63,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("server_not_support", "Пакуль не падтрымліваецца серверам"), ("Not available", "Недаступна"), ("Too frequent", "Занадта часта"), - ("Cancel", "Адмяніць"), + ("Cancel", "Скасаваць"), ("Skip", "Прапусціць"), ("Close", "Закрыць"), - ("Retry", "Паўтор"), + ("Retry", "Паўтарыць спробу"), ("OK", "ОК"), ("Password Required", "Патрабуецца пароль"), ("Please enter your password", "Увядзіце пароль"), @@ -75,14 +75,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Do you want to enter again?", "Паўтарыць уваход?"), ("Connection Error", "Памылка падключэння"), ("Error", "Памылка"), - ("Reset by the peer", "Скінута выдаленым вузлом"), + ("Reset by the peer", "Скінута абанентам"), ("Connecting...", "Падключэнне..."), - ("Connection in progress. Please wait.", "Выконваецца падключэнне. Пачакайце."), + ("Connection in progress. Please wait.", "Ідзе падключэнне. Пачакайце."), ("Please try 1 minute later", "Паспрабуйце праз хвіліну"), ("Login Error", "Памылка ўваходу"), ("Successful", "Паспяхова"), - ("Connected, waiting for image...", "Падключана, чаканне выявы..."), - ("Name", "Імя"), + ("Connected, waiting for image...", "Падключана, чаканне відарыса..."), + ("Name", "Назва"), ("Type", "Тып"), ("Modified", "Зменена"), ("Size", "Памер"), @@ -91,78 +91,78 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Send", "Адправіць"), ("Refresh File", "Абнавіць файл"), ("Local", "Лакальны"), - ("Remote", "Выдалены"), - ("Remote Computer", "Выдалены камп'ютар"), + ("Remote", "Аддалены"), + ("Remote Computer", "Аддалены камп'ютар"), ("Local Computer", "Лакальны камп'ютар"), ("Confirm Delete", "Пацвердзіць выдаленне"), ("Delete", "Выдаліць"), ("Properties", "Уласцівасці"), ("Multi Select", "Шматлікі выбар"), - ("Select All", "Абраць усе"), - ("Unselect All", "Зняць усе"), - ("Empty Directory", "Пустая тэчка"), - ("Not an empty directory", "Тэчка не пустая"), + ("Select All", "Выбраць усе"), + ("Unselect All", "Скасаваць выбар усіх"), + ("Empty Directory", "Пусты каталог"), + ("Not an empty directory", "Каталог не пусты"), ("Are you sure you want to delete this file?", "Выдаліць гэты файл?"), - ("Are you sure you want to delete this empty directory?", "Выдаліць пустую тэчку?"), - ("Are you sure you want to delete the file of this directory?", "Выдаліць файл з гэтай тэчкі?"), + ("Are you sure you want to delete this empty directory?", "Выдаліць пусты каталог?"), + ("Are you sure you want to delete the file of this directory?", "Выдаліць файл з гэтага каталога?"), ("Do this for all conflicts", "Прымяніць да ўсіх канфліктаў"), - ("This is irreversible!", "Гэта неабаротна!"), - ("Deleting", "Выдаленне"), + ("This is irreversible!", "Гэтага нельга адрабіць!"), + ("Deleting", "Ідзе выдаленне"), ("files", "файлы"), ("Waiting", "Чаканне"), ("Finished", "Завершана"), ("Speed", "Хуткасць"), - ("Custom Image Quality", "Якасць выявы па запыце"), - ("Privacy mode", "Рэжым прыватнасці"), - ("Block user input", "Забараніць ўвод на аддаленай прыладзе"), - ("Unblock user input", "Адблакіраваць ўвод на аддаленай прыладзе"), + ("Custom Image Quality", "Карыстальніцкая якасць відарыса"), + ("Privacy mode", "Рэжым канфідэнцыйнасці"), + ("Block user input", "Заблакіраваць увод на аддаленай прыладзе"), + ("Unblock user input", "Разблакіраваць увод на аддаленай прыладзе"), ("Adjust Window", "Наладзіць акно"), ("Original", "Арыгінал"), ("Shrink", "Сціснуць"), ("Stretch", "Расцягнуць"), - ("Scrollbar", "Паласа пракруткі"), - ("ScrollAuto", "Аўта-пракрутка"), - ("Good image quality", "Добрая якасць выявы"), - ("Balanced", "Баланс паміж якасцю і адказам"), - ("Optimize reaction time", "Оптымізацыя часу адказу"), - ("Custom", "Зададзена карыстальнікам"), + ("Scrollbar", "Паласа прагортвання"), + ("ScrollAuto", "Аўта-прагортванне"), + ("Good image quality", "Добрая якасць відарыса"), + ("Balanced", "Баланс паміж якасцю і хуткасцю"), + ("Optimize reaction time", "Аптымізацыя хуткасці рэакцыі"), + ("Custom", "Карыстальніцкая"), ("Show remote cursor", "Паказваць аддалены курсор"), ("Show quality monitor", "Паказваць манітор якасці"), ("Disable clipboard", "Адключыць буфер абмену"), - ("Lock after session end", "Заблакаваць уліковы запіс пасля сеансу"), + ("Lock after session end", "Заблакіраваць уліковы запіс пасля сеанса"), ("Insert Ctrl + Alt + Del", "Уставіць Ctrl + Alt + Del"), - ("Insert Lock", "Заблакаваць уліковы запіс"), + ("Insert Lock", "Заблакіраваць уліковы запіс"), ("Refresh", "Абнавіць"), ("ID does not exist", "ID не існуе"), - ("Failed to connect to rendezvous server", "Немагчыма падключыцца да паседкавага сервера"), + ("Failed to connect to rendezvous server", "Немагчыма падключыцца да прамежкавага сервера"), ("Please try later", "Паспрабуйце пазней"), ("Remote desktop is offline", "Аддаленая прылада не ў сетцы"), ("Key mismatch", "Неадпаведнасць ключоў"), ("Timeout", "Час чакання скончыўся"), ("Failed to connect to relay server", "Немагчыма падключыцца да рэтранслятара"), - ("Failed to connect via rendezvous server", "Немагчыма падключыцца праз паседкавы сервер"), + ("Failed to connect via rendezvous server", "Немагчыма падключыцца праз прамежкавы сервер"), ("Failed to connect via relay server", "Немагчыма падключыцца праз рэтранслятар"), - ("Failed to make direct connection to remote desktop", "Не ўдалося ўсталяваць прамое падключэнне да аддаленага працоўнага стала"), - ("Set Password", "Усталяваць пароль"), - ("OS Password", "Пароль ўваходу ў аперацыйную сістэму"), - ("install_tip", "У некаторых выпадках RustDesk можа працаваць няправільна на аддаленым вузле з-за UAC. Каб пазбегнуць магчымых праблем з UAC, націсніце кнопку ніжэй для ўстаноўкі RustDesk у сістэме."), + ("Failed to make direct connection to remote desktop", "Не ўдалося ўсталяваць прамога падключэння да аддаленай прылады"), + ("Set Password", "Задаць пароль"), + ("OS Password", "Пароль уваходу ў аперацыйную сістэму"), + ("install_tip", "У некаторых выпадках з-за UAC, RustDesk можа працаваць на баку абанента неадпаведным чынам. Каб пазбегнуць магчымых праблем з UAC, націсніце кнопку ніжэй для ўсталявання RustDesk у сістэме."), ("Click to upgrade", "Абнавіць"), ("Configure", "Наладзіць"), - ("config_acc", "Каб аддаленна кіраваць сваім працоўным сталом, вам неабходна дазволіць RustDesk правы доступу."), - ("config_screen", "Для аддаленага доступу да працоўнага сталу вам неабходна дазволіць RustDesk правы здымку экрана."), - ("Installing ...", "Ідзе ўстаноўка..."), + ("config_acc", "Каб аддаленна кіраваць сваім працоўным сталом, вам трэба дазволіць RustDesk правы \"доступу\""), + ("config_screen", "Для аддаленага доступу да працоўнага стала вам трэба даць RustDesk правы \"здымку экрана\"."), + ("Installing ...", "Ідзе ўсталёўванне..."), ("Install", "Усталяваць"), - ("Installation", "Устаноўка"), - ("Installation Path", "Шлях устаноўкі"), + ("Installation", "Усталёўванне"), + ("Installation Path", "Шлях усталёўвання"), ("Create start menu shortcuts", "Стварыць ярлыкі ў меню \"Пуск\""), ("Create desktop icon", "Стварыць значок на працоўным стале"), - ("agreement_tip", "Пачынаючы ўстаноўку, вы прымаеце ўмовы ліцэнзійнага ўгоды."), + ("agreement_tip", "Пачынаючы ўсталёўванне, вы прымаеце ўмовы ліцэнзійнага пагаднення."), ("Accept and Install", "Прыняць і ўсталяваць"), - ("End-user license agreement", "Ліцэнзійная ўгода з канчатковым карыстальнікам"), - ("Generating ...", "Генеруецца..."), - ("Your installation is lower version.", "Ваша ўстаноўка ніжэйшай версіі"), - ("not_close_tcp_tip", "Не зачыняць гэта акно пры выкарыстанні тунэлю."), - ("Listening ...", "Праслухоўванне..."), + ("End-user license agreement", "Ліцэнзійнае пагадненне з канчатковым карыстальнікам"), + ("Generating ...", "Ідзе генерыраванне..."), + ("Your installation is lower version.", "Усталявана ранейшая версія"), + ("not_close_tcp_tip", "Не закрываць гэтага акна пры выкарыстанні тунэлю."), + ("Listening ...", "Чаканне..."), ("Remote Host", "Аддалены хост"), ("Remote Port", "Аддалены порт"), ("Action", "Дзеянне"), @@ -170,120 +170,120 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local Port", "Лакальны порт"), ("Local Address", "Лакальны адрас"), ("Change Local Port", "Змяніць лакальны порт"), - ("setup_server_tip", "Для хуткага падключэння наладзьце свой сервер."), + ("setup_server_tip", "Для хутчэйшага падключэння наладзьце ўласны сервер."), ("Too short, at least 6 characters.", "Занадта кароткі, мінімум 6 сімвалаў."), - ("The confirmation is not identical.", "Пацверджанне не супадае."), + ("The confirmation is not identical.", "Пацвярджэнне не супадае."), ("Permissions", "Дазволы"), ("Accept", "Прыняць"), ("Dismiss", "Адхіліць"), ("Disconnect", "Адключыць"), - ("Enable file copy and paste", "Дазволіць капіраванне і ўстаўку файлаў"), + ("Enable file copy and paste", "Дазволіць капіяванне і ўстаўку файлаў"), ("Connected", "Падключана"), ("Direct and encrypted connection", "Прамое і зашыфраванае падключэнне"), ("Relayed and encrypted connection", "Рэтрансляванае і зашыфраванае падключэнне"), ("Direct and unencrypted connection", "Прамое і незашыфраванае падключэнне"), ("Relayed and unencrypted connection", "Рэтрансляванае і незашыфраванае падключэнне"), - ("Enter Remote ID", "Увядзіце дыстанцыйны ID"), + ("Enter Remote ID", "Увядзіце ID абанента"), ("Enter your password", "Увядзіце пароль"), - ("Logging in...", "Уваход..."), - ("Enable RDP session sharing", "Дазволіць абмен сеансамі RDP"), - ("Auto Login", "Аўтаматычны ўваход у ўліковы запіс"), + ("Logging in...", "Уваходжанне..."), + ("Enable RDP session sharing", "Уключыць абагульванне сеанса RDP"), + ("Auto Login", "Аўтаматычны ўваход ва ўліковы запіс"), ("Enable direct IP access", "Дазволіць прамы доступ па IP-адрасе"), ("Rename", "Перайменаваць"), ("Space", "Месца"), ("Create desktop shortcut", "Стварыць ярлык на працоўным стале"), ("Change Path", "Змяніць шлях"), - ("Create Folder", "Стварыць тэчку"), - ("Please enter the folder name", "Калі ласка, увядзіце імя тэчкі"), + ("Create Folder", "Стварыць папку"), + ("Please enter the folder name", "Увядзіце імя папкі"), ("Fix it", "Выправіць"), ("Warning", "Папярэджанне"), ("Login screen using Wayland is not supported", "Уваход у сістэму з выкарыстаннем Wayland не падтрымліваецца"), ("Reboot required", "Патрабуецца перазагрузка"), - ("Unsupported display server", "Непадтрымліваемы сервер адлюстравання"), + ("Unsupported display server", "Сервер адлюстравання не падтрымліваецца"), ("x11 expected", "Чакаецца X11"), ("Port", "Порт"), ("Settings", "Налады"), ("Username", "Імя карыстальніка"), - ("Invalid port", "Няправільны порт"), - ("Closed manually by the peer", "Зачынена аддаленым вузлом уручную"), - ("Enable remote configuration modification", "Дазволіць змену канфігурацыі аддалена"), - ("Run without install", "Запусціць без ўстаноўкі"), + ("Invalid port", "Памылковы порт"), + ("Closed manually by the peer", "Закрыта абанентам уручную"), + ("Enable remote configuration modification", "Дазволіць аддаленае змяненне канфігурацыі"), + ("Run without install", "Запусціць без усталявання"), ("Connect via relay", "Падключыцца праз рэтранслятар"), ("Always connect via relay", "Заўсёды падключацца праз рэтранслятар"), - ("whitelist_tip", "Толькі IP-адрэсы з белага спісу могуць атрымаць доступ да маёй прылады."), + ("whitelist_tip", "Атрымліваць доступ да маёй прылады могуць толькі IP-адрасы з белага спісу."), ("Login", "Увайсці"), ("Verify", "Праверыць"), - ("Remember me", "Запомніць мяне"), - ("Trust this device", "Даверыць гэтую прыладу"), + ("Remember me", "Запомніць"), + ("Trust this device", "Давяраць гэтай прыладзе"), ("Verification code", "Праверачны код"), - ("verification_tip", "Выяўлена новая прылада, на зарэгістраваны адрас электроннай пошты адпраўлены праверачны код. Увядзіце яго, каб працягнуць уваход у сістэму."), + ("verification_tip", "Выяўлена новая прылада, на зарэгістраваны адрас электроннай пошты адпраўлены праверачны код. Увядзіце яго, каб працягнуць уваходжанне ў сістэму."), ("Logout", "Выйсці"), - ("Tags", "Тэгі"), + ("Tags", "Цэтлікі"), ("Search ID", "Пошук по ID"), - ("whitelist_sep", "Аддзяліць запятой, коскай з запятой, прабелам ці новым радком."), + ("whitelist_sep", "Падзяленне коскай, кропкай з коскай, прабелам або новым радком."), ("Add ID", "Дадаць ID"), - ("Add Tag", "Дадаць тэг"), - ("Unselect all tags", "Скасаваць выбар усіх тэгаў"), + ("Add Tag", "Дадаць цэтлік"), + ("Unselect all tags", "Скасаваць выбар усіх цэтлікаў"), ("Network error", "Памылка сеткі"), - ("Username missed", "Адсутнічае імя карыстальніка"), - ("Password missed", "Забыты пароль"), - ("Wrong credentials", "Няправільныя імя ці пароль"), - ("The verification code is incorrect or has expired", "Праверачны код няправільны або скончыўся тэрмін яго дзеяння"), - ("Edit Tag", "Рэдагаваць тэг"), - ("Forget Password", "Забыць пароль"), + ("Username missed", "Прапушчана імя карыстальніка"), + ("Password missed", "Прапушчаны пароль"), + ("Wrong credentials", "Памылковае імя або пароль"), + ("The verification code is incorrect or has expired", "Памылковы або пратэрмінаваны праверачны код"), + ("Edit Tag", "Рэдагаваць цэтлік"), + ("Forget Password", "Не захоўваць пароль"), ("Favorites", "Абранае"), ("Add to Favorites", "Дадаць у абранае"), ("Remove from Favorites", "Выдаліць з абранага"), ("Empty", "Пуста"), - ("Invalid folder name", "Недапушчальнае імя тэчкі"), + ("Invalid folder name", "Недапушчальная назва папкі"), ("Socks5 Proxy", "Socks5-проксі"), ("Socks5/Http(s) Proxy", "Socks5/Http(s)-проксі"), ("Discovered", "Знойдзена"), - ("install_daemon_tip", "Для запуску пры загрузцы неабходна ўстанавіць сістэмную службу"), - ("Remote ID", "Аддалены ID"), + ("install_daemon_tip", "Для запуску пры загрузцы трэба ўсталяваць сістэмную службу"), + ("Remote ID", "ID абанента"), ("Paste", "Уставіць"), - ("Paste here?", "Уставіць тут?"), - ("Are you sure to close the connection?", "Ці ўпэўненыя, што жадаеце закрыць падключэнне?"), + ("Paste here?", "Уставіць сюды?"), + ("Are you sure to close the connection?", "Закрыць падключэнне?"), ("Download new version", "Спампаваць новую версію"), ("Touch mode", "Рэжым сэнсарнага экрана"), - ("Mouse mode", "Рэжым мышы/трэкпада"), - ("One-Finger Tap", "Націск адным пальцам"), + ("Mouse mode", "Рэжым мышы/сэнсарнай панэлі"), + ("One-Finger Tap", "Націсканне адным пальцам"), ("Left Mouse", "Левая кнопка мышы"), - ("One-Long Tap", "Доўгі націск адным пальцам"), - ("Two-Finger Tap", "Націск двума пальцамі"), + ("One-Long Tap", "Доўгае націсканне адным пальцам"), + ("Two-Finger Tap", "Націсканне двума пальцамі"), ("Right Mouse", "Правая кнопка мышы"), ("One-Finger Move", "Перамяшчэнне адным пальцам"), - ("Double Tap & Move", "Двайны націск і перамяшчэнне"), + ("Double Tap & Move", "Двайное націсканне і перамяшчэнне"), ("Mouse Drag", "Перацягванне мышшу"), ("Three-Finger vertically", "Трыма пальцамі па вертыкалі"), - ("Mouse Wheel", "Кола мышы"), + ("Mouse Wheel", "Колца мышы"), ("Two-Finger Move", "Перамяшчэнне двума пальцамі"), ("Canvas Move", "Перамяшчэнне палатна"), - ("Pinch to Zoom", "Маштабаванне сціскам"), - ("Canvas Zoom", "Маштаб палатна"), - ("Reset canvas", "Скінуць палатно"), + ("Pinch to Zoom", "Маштабаванне шчыпком"), + ("Canvas Zoom", "Маштабаванне палатна"), + ("Reset canvas", "Скінуць маштабаванне палатна"), ("No permission of file transfer", "Няма дазволу на перадачу файлаў"), ("Note", "Нататка"), ("Connection", "Падключэнне"), - ("Share screen", "Дзяліцца экранам"), + ("Share screen", "Дэманстрацыя экрана"), ("Chat", "Чат"), ("Total", "Усяго"), ("items", "элементы"), ("Selected", "Выбрана"), ("Screen Capture", "Захоп экрана"), ("Input Control", "Кіраванне ўводам"), - ("Audio Capture", "Захоп аўдыё"), - ("Do you accept?", "Ці вы згодны?"), + ("Audio Capture", "Захоп аўдыя"), + ("Do you accept?", "Вы згодныя?"), ("Open System Setting", "Адкрыць налады сістэмы"), ("How to get Android input permission?", "Як атрымаць дазвол на ўвод Android?"), - ("android_input_permission_tip1", "Каб аддалёная прылада магла кіраваць вашай Android-прыладай з дапамогай мышы або націсканняў, неабходна дазволіць RustDesk выкарыстоўваць паслугу \"Асаблівыя магчымасці\"."), - ("android_input_permission_tip2", "Зайдзіце на адпаведную старонку сістэмных налад, знайдзіце і ўступіце ў \"Устаноўленыя паслугі\", уключыце паслугу \"RustDesk Input\"."), - ("android_new_connection_tip", "Атрыманы запыт на кіраванне вашай бягучай прыладай."), - ("android_service_will_start_tip", "Уключэнне захопу экрана аўтаматычна запускае службу, дазваляючы іншым прыладам запытаць падлучэнне да гэтай прылады."), - ("android_stop_service_tip", "Закрыццё службы аўтаматычна зачыніць усе ўстаноўленыя падлучэнні."), - ("android_version_audio_tip", "Бягучая версія Android не падтрымлівае захоп звуку, абнавіце яе да Android 10 ці вышэй."), + ("android_input_permission_tip1", "Каб аддаленая прылада магла кіраваць вашай Android-прыладай з дапамогай мышы або націсканняў, трэба дазволіць RustDesk выкарыстоўваць службу \"Спецыяльныя магчымасці\"."), + ("android_input_permission_tip2", "Зайдзіце на адпаведную старонку сістэмных налад, знайдзіце і перайдзіце ва \"Усталяваныя службы\", уключыце службу \"RustDesk Input\"."), + ("android_new_connection_tip", "Новы запыт на кіраванне вашай бягучай прыладай."), + ("android_service_will_start_tip", "Уключэнне захопу экрана аўтаматычна запускае службу, дазваляючы іншым прыладам запытаць падключэнне да гэтай прылады."), + ("android_stop_service_tip", "Закрыццё службы аўтаматычна закрые ўсе ўстаноўленыя падключэнні."), + ("android_version_audio_tip", "Бягучая версія Android не падтрымлівае захопу гуку, абнавіце яе да Android 10 ці вышэй."), ("android_start_service_tip", "Націсніце [Запусціць службу] або дазвольце [Захоп экрана], каб запусціць службу дэманстрацыі экрана."), - ("android_permission_may_not_change_tip", "Дазволы для ўстаноўленых падлучэнняў не могуць быць змененыя, неабходна перападключэнне."), + ("android_permission_may_not_change_tip", "Дазволы для ўстаноўленых падключэнняў не могуць быць зменены, патрабуецца перападключэнне."), ("Account", "Уліковы запіс"), ("Overwrite", "Перазапісаць"), ("This file exists, skip or overwrite this file?", "Файл існуе, прапусціць ці перазапісаць яго?"), @@ -291,47 +291,47 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Help", "Дапамога"), ("Failed", "Не ўдалося"), ("Succeeded", "Выканана"), - ("Someone turns on privacy mode, exit", "Хтосьці ўключыў рэжым прыватнасці, выхад"), + ("Someone turns on privacy mode, exit", "Хтосьці ўключыў рэжым канфідэнцыйнасці, выхад"), ("Unsupported", "Не падтрымліваецца"), - ("Peer denied", "Адмоўлена аддаленым вузлом"), - ("Please install plugins", "Усталюйце плагіны"), - ("Peer exit", "Аддалены вузел адключаны"), - ("Failed to turn off", "Немагчыма адключыць"), - ("Turned off", "Адключаны"), + ("Peer denied", "Забаронена абанентам"), + ("Please install plugins", "Усталюйце ўбудовы"), + ("Peer exit", "Абанент выйшаў"), + ("Failed to turn off", "Немагчыма выключыць"), + ("Turned off", "Выключаны"), ("Language", "Мова"), ("Keep RustDesk background service", "Захаваць фонавую службу RustDesk"), - ("Ignore Battery Optimizations", "Ігнараваць аптымізацыю патрэблення батарэі"), + ("Ignore Battery Optimizations", "Ігнараваць аптымізацыю ўжывання батарэі"), ("android_open_battery_optimizations_tip", "Перайдзіце на наступную старонку налад"), ("Start on boot", "Запускаць пры загрузцы"), ("Start the screen sharing service on boot, requires special permissions", "Запускаць службу дэманстрацыі экрана пры загрузцы (патрабуюцца спецыяльныя дазволы)"), ("Connection not allowed", "Падключэнне не дазволена"), - ("Legacy mode", "Стары рэжым"), + ("Legacy mode", "Састарэлы рэжым"), ("Map mode", "Рэжым супастаўлення"), ("Translate mode", "Рэжым перакладу"), ("Use permanent password", "Выкарыстоўваць пастаянны пароль"), ("Use both passwords", "Выкарыстоўваць абодва паролі"), - ("Set permanent password", "Устанавіць пастаянны пароль"), + ("Set permanent password", "Задаць пастаянны пароль"), ("Enable remote restart", "Дазволіць аддалены перазапуск"), ("Restart remote device", "Перазапусціць аддаленую прыладу"), - ("Are you sure you want to restart", "Вы ўпэўненыя, што хочаце перазагрузіць?"), - ("Restarting remote device", "Перазапуск аддаленай прылады"), - ("remote_restarting_tip", "Аддаленая прылада перазапускаецца. Закрыйце гэтае паведамленне і праз некаторы час перападключыцеся, выкарыстоўваючы пастаянны пароль."), - ("Copied", "Скапіравана"), + ("Are you sure you want to restart", "Вы ўпэўненыя, што хочаце зрабіць перазапуск?"), + ("Restarting remote device", "Ідзе перазапуск аддаленай прылады"), + ("remote_restarting_tip", "Аддаленая прылада перазапускаецца. Закрыйце гэта паведамленне і праз некаторы час перападключыцеся, выкарыстоўваючы пастаянны пароль."), + ("Copied", "Скапіявана"), ("Exit Fullscreen", "Выйсці з поўнаэкраннага рэжыму"), ("Fullscreen", "Поўнаэкранны рэжым"), ("Mobile Actions", "Мабільныя дзеянні"), - ("Select Monitor", "Выбраць манітор"), - ("Control Actions", "Дзеянні па кіраванню"), + ("Select Monitor", "Выберыце манітор"), + ("Control Actions", "Дзеянні па кіраванні"), ("Display Settings", "Налады адлюстравання"), ("Ratio", "Суадносіны"), - ("Image Quality", "Якасць выявы"), - ("Scroll Style", "Стыль пракруткі"), + ("Image Quality", "Якасць відарыса"), + ("Scroll Style", "Стыль прагортвання"), ("Show Toolbar", "Паказаць панэль інструментаў"), ("Hide Toolbar", "Схаваць панэль інструментаў"), - ("Direct Connection", "Прамаое злучэнне"), - ("Relay Connection", "Рэтрансляванае злучэнне"), - ("Secure Connection", "Бяспечнае злучэнне"), - ("Insecure Connection", "Нябяспечнае злучэнне"), + ("Direct Connection", "Прамое падключэнне"), + ("Relay Connection", "Рэтрансляванае падключэнне"), + ("Secure Connection", "Бяспечнае падключэнне"), + ("Insecure Connection", "Нябяспечнае падключэнне"), ("Scale original", "Арыгінальны маштаб"), ("Scale adaptive", "Адаптыўны маштаб"), ("General", "Агульныя"), @@ -339,13 +339,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Theme", "Тэма"), ("Dark Theme", "Цёмная тэма"), ("Light Theme", "Светлая тэма"), - ("Dark", "Цёмны"), - ("Light", "Светлы"), - ("Follow System", "Прытрымлівацца сістэмы"), + ("Dark", "Цёмная"), + ("Light", "Светлая"), + ("Follow System", "Сістэмная"), ("Enable hardware codec", "Уключыць апаратны кодэк"), - ("Unlock Security Settings", "Разблакаваць налады бяспекі"), + ("Unlock Security Settings", "Разблакіраваць налады бяспекі"), ("Enable audio", "Уключыць перадачу гуку"), - ("Unlock Network Settings", "Разблакаваць сеткавыя налады"), + ("Unlock Network Settings", "Разблакіраваць сеткавыя налады"), ("Server", "Сервер"), ("Direct IP Access", "Прамы IP-доступ"), ("Proxy", "Проксі"), @@ -358,7 +358,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Pin Toolbar", "Закрэпіць панэль інструментаў"), ("Unpin Toolbar", "Адкрэпіць панэль інструментаў"), ("Recording", "Запіс"), - ("Directory", "Тэчка"), + ("Directory", "Каталог"), ("Automatically record incoming sessions", "Аўтаматычна запісваць уваходныя сесіі"), ("Automatically record outgoing sessions", ""), ("Change", "Змяніць"), @@ -370,35 +370,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Write a message", "Напісаць паведамленне"), ("Prompt", "Падказка"), ("Please wait for confirmation of UAC...", "Дачакайцеся пацверджання UAC..."), - ("elevated_foreground_window_tip", "Бягучае акно аддаленага працоўнага стала патрабуе вышэйшых прывілегій для працы, таму часова немагчыма выкарыстоўваць мыш і клавіятуру. Можна папрасіць аддаленага карыстальніка згорнуць бягучае акно або націснуць кнопку павышэння правоў у акне кіравання падлучэннем. Каб прадухіліць гэтую праблему ў будучыні, рэкамендуецца ўстанавіць праграмнае забеспячэнне на аддаленай прыладзе."), + ("elevated_foreground_window_tip", "Бягучае акно аддаленага працоўнага стала патрабуе вышэйшых прывілегій для працы, таму часова немагчыма выкарыстоўваць мыш і клавіятуру. Можна папрасіць абанента згарнуць бягучае акно або націснуць кнопку павышэння правоў у акне кіравання падключэннем. Каб прадухіліць гэту праблему ў будучыні, рэкамендуецца ўсталяваць праграмнае забеспячэнне на аддаленай прыладзе."), ("Disconnected", "Адключана"), ("Other", "Іншае"), - ("Confirm before closing multiple tabs", "Пацвердзіць закрыццё некалькіх ўкладак"), + ("Confirm before closing multiple tabs", "Пацвердзіць закрыццё некалькіх укладак"), ("Keyboard Settings", "Налады клавіятуры"), ("Full Access", "Поўны доступ"), ("Screen Share", "Дэманстрацыя экрана"), ("ubuntu-21-04-required", "Wayland патрабуе Ubuntu версіі 21.04 або навейшай."), - ("wayland-requires-higher-linux-version", "Для Wayland патрабуецца вышэйшая версія дыстрыбутыву Linux. Карыстайцеся працоўным сталом X11 або зменіце сваю АС."), + ("wayland-requires-higher-linux-version", "Для Wayland патрабуецца вышэйшая версія дыстрыбутыва Linux. Карыстайцеся працоўным сталом X11 або зменіце сваю АС."), ("xdp-portal-unavailable", ""), - ("JumpLink", "Перайсці па спасылцы"), - ("Please Select the screen to be shared(Operate on the peer side).", "Выберыце экран для дэманстрацыі (кіруецца аддаленай стараной)."), + ("JumpLink", "Прагляд"), + ("Please Select the screen to be shared(Operate on the peer side).", "Выберыце экран для дэманстрацыі (кіруецца на баку абанента)."), ("Show RustDesk", "Паказаць RustDesk"), - ("This PC", "Гэты кампутар"), + ("This PC", "Гэты камп’ютар"), ("or", "або"), ("Elevate", "Павысіць"), - ("Zoom cursor", "Павялічэнне курсора"), + ("Zoom cursor", "Маштабаванне курсора"), ("Accept sessions via password", "Прымаць сеансы па паролю"), ("Accept sessions via click", "Прымаць сеансы націскам кнопкі"), ("Accept sessions via both", "Прымаць сеансы па паролю і націскам кнопкі"), - ("Please wait for the remote side to accept your session request...", "Дачакайцеся, пакуль аддаленая старана прыме ваш запыт на сеанс..."), + ("Please wait for the remote side to accept your session request...", "Дачакайцеся, пакуль абанент прымае ваш запыт на сеанс..."), ("One-time Password", "Аднаразовы пароль"), ("Use one-time password", "Выкарыстоўваць аднаразовы пароль"), ("One-time password length", "Даўжыня аднагаразовага пароля"), ("Request access to your device", "Запыт на доступ да вашай прылады"), - ("Hide connection management window", "Схаваць акно кіравання падлучэннямі"), + ("Hide connection management window", "Схаваць акно кіравання падключэннямі"), ("hide_cm_tip", "Дазваляць схаванне акна ў выпадку, калі прымаюцца сесіі па паролю або выкарыстоўваецца пастаянны пароль"), - ("wayland_experiment_tip", "Падтрымка Wayland знаходзіцца на эксперыментальнай стадыі, калі вам неабходны аўтаматычны доступ, выкарыстоўвайце X11."), - ("Right click to select tabs", "Правы клік для выбару ўкладак"), + ("wayland_experiment_tip", "Падтрымка Wayland знаходзіцца на эксперыментальнай стадыі, калі вам трэба аўтаматычны доступ, выкарыстоўвайце X11."), + ("Right click to select tabs", "Выбар укладак націсканнем правай кнопкі мышы"), ("Skipped", "Прапушчана"), ("Add to address book", "Дадаць у адрасную кнігу"), ("Group", "Група"), @@ -406,71 +406,71 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by web console", "Закрыта ўручную праз вэб-кансоль"), ("Local keyboard type", "Тып лакальнай клавіятуры"), ("Select local keyboard type", "Выберыце тып лакальнай клавіятуры"), - ("software_render_tip", "Калі ў вас ёсць відэакарта Nvidia і аддаленае акно зачыняецца адразу пасля падлучэння, магчыма, дапаможа ўстаноўка драйвера Nouveau і выбар выкарыстання праграмнай візуалізацыі. Патрабуецца перазагрузка."), + ("software_render_tip", "Калі ў вас ёсць відэакарта Nvidia і аддаленае акно закрываецца адразу пасля падключэння, магчыма, дапаможа ўсталяванне драйвера Nouveau і выбар выкарыстання праграмнай візуалізацыі. Патрабуецца перазагрузка."), ("Always use software rendering", "Заўсёды выкарыстоўваць праграмную візуалізацыю"), - ("config_input", "Каб кіраваць аддаленым працоўным сталом праз клавіятуру, неабходна дазволіць RustDesk маніторынг уводу."), - ("config_microphone", "Каб размаўляць з аддаленай старонкай, неабходна дазволіць RustDesk запіс аўдыё."), - ("request_elevation_tip", "Таксама можна запытаць павышэнне правоў, калі хто-небудзь знаходзіцца на аддаленай старонцы."), + ("config_input", "Каб кіраваць аддаленым працоўным сталом праз клавіятуру, трэба дазволіць RustDesk \"Маніторынг уводу\"."), + ("config_microphone", "Каб размаўляць з абанентам, трэба дазволіць RustDesk запіс аўдыя."), + ("request_elevation_tip", "Таксама можна запытаць павышэння правоў, калі хто-небудзь знаходзіцца на баку абанента."), ("Wait", "Чакайце"), ("Elevation Error", "Памылка павышэння правоў"), - ("Ask the remote user for authentication", "Запытаць аўтэнтыфікацыю ў аддаленага карыстальніка"), - ("Choose this if the remote account is administrator", "Выберыце гэта, калі аддалены акаўнт з'яўляецца адміністратарам"), + ("Ask the remote user for authentication", "Запытаць праверку сапраўднасці ў абанента"), + ("Choose this if the remote account is administrator", "Выберыце гэта, калі абанент з'яўляецца адміністратарам"), ("Transmit the username and password of administrator", "Перадаць імя карыстальніка і пароль адміністратара"), - ("still_click_uac_tip", "Дагэтуль патрэбна, каб аддалены карыстальнік націснуў \"OK\" ў акне UAC пры запуску RustDesk."), - ("Request Elevation", "Запыт на павышэнне"), - ("wait_accept_uac_tip", "Пачакайце, пакуль аддалены карыстальнік пацвердзіць запыт UAC."), - ("Elevate successfully", "Павышэнне паспяхова выканана"), - ("uppercase", "Вялікія літары"), - ("lowercase", "Малыя літары"), - ("digit", "Лічбы"), - ("special character", "Спецыяльныя сімвалы"), - ("length>=8", "Даўжыня >= 8 сімвалаў"), + ("still_click_uac_tip", "Дагэтуль патрэбна, каб абанент націснуў \"OK\" ў акне UAC пры запуску RustDesk."), + ("Request Elevation", "Запытаць павышэння"), + ("wait_accept_uac_tip", "Пачакайце, пакуль абанент пацвердзіць запыт UAC."), + ("Elevate successfully", "Правы павышаны"), + ("uppercase", "верхні рэгістр"), + ("lowercase", "ніжні рэгістр"), + ("digit", "лічбы"), + ("special character", "спецыяльныя сімвалы"), + ("length>=8", "8+ сімвалаў"), ("Weak", "Слабы"), ("Medium", "Сярэдні"), ("Strong", "Моцны"), ("Switch Sides", "Пераключыць бакі"), - ("Please confirm if you want to share your desktop?", "Пацвердзіце, калі хочаце дазволіць паказ вашага працоўнага стала?"), + ("Please confirm if you want to share your desktop?", "Вы сапраўды дазваляеце дэманстрацыю працоўнага стала?"), ("Display", "Адлюстраванне"), - ("Default View Style", "Стыль адлюстравання па змаўчанні"), - ("Default Scroll Style", "Стыль пракруткі па змаўчанні"), - ("Default Image Quality", "Якасць выявы па змаўчанні"), - ("Default Codec", "Кодэк па змаўчанні"), + ("Default View Style", "Стандартны стыль адлюстравання"), + ("Default Scroll Style", "Стандартны стыль прагортвання"), + ("Default Image Quality", "Стандартная якасць відарыса"), + ("Default Codec", "Стандартны кодэк"), ("Bitrate", "Бітрэйт"), ("FPS", "Колькасць кадраў у секунду"), ("Auto", "Аўта"), - ("Other Default Options", "Іншыя параметры па змаўчанні"), + ("Other Default Options", "Іншыя стандартныя параметры"), ("Voice call", "Галасавы выклік"), ("Text chat", "Тэкставы чат"), ("Stop voice call", "Спыніць галасавы выклік"), - ("relay_hint_tip", "Непасрэднае падключэнне можа быць немагчымым. У гэтым выпадку можна спрабаваць падключыцца праз рэлей.\nАкрамя таго, калі вы хочаце адразу выкарыстоўваць рэлей, можна дадаць да ідэнтыфікатара суфікс \"/r\" або ўключыць \"Заўсёды падключацца праз рэлей\" ў наладах аддаленага вузла."), + ("relay_hint_tip", "Непасрэднае падключэнне можа быць немагчымым. У гэтым выпадку можна спрабаваць падключыцца праз рэтранслятар.\nАкрамя таго, калі вы хочаце адразу выкарыстоўваць рэтранслятар, можна дадаць да ідэнтыфікатара суфікс \"/r\" або ўключыць \"Заўсёды падключацца праз рэтранслятар\" у наладах абанента."), ("Reconnect", "Перападключыць"), ("Codec", "Кодэк"), - ("Resolution", "Разрознасць"), + ("Resolution", "Раздзяляльнасць"), ("No transfers in progress", "Перадача не ажыццяўляецца"), ("Set one-time password length", "Усталяваць даўжыню аднаразовага пароля"), ("RDP Settings", "Налады RDP"), ("Sort by", "Сартаваць па"), - ("New Connection", "Новае злучэнне"), + ("New Connection", "Новае падключэнне"), ("Restore", "Аднавіць"), ("Minimize", "Згарнуць"), ("Maximize", "Разгарнуць"), ("Your Device", "Ваша прылада"), ("empty_recent_tip", "Няма апошніх сеансаў!\nЧас запланаваць новы."), - ("empty_favorite_tip", "Яшчэ няма выбраных аддаленых вузлоў?\nДавайце знойдзем, каго можна дадаць у выбранае."), - ("empty_lan_tip", "Не знойдзены аддаленыя вузлы."), - ("empty_address_book_tip", "У адраснай кнізе няма аддаленых вузлоў."), + ("empty_favorite_tip", "Яшчэ няма абраных абанентаў?\nДавайце знойдзем, каго можна дадаць у абранае."), + ("empty_lan_tip", "Абанентаў не знойдзена."), + ("empty_address_book_tip", "У адраснай кнізе няма абанентаў."), ("Empty Username", "Пустае імя карыстальніка"), ("Empty Password", "Пусты пароль"), ("Me", "Я"), - ("identical_file_tip", "Файл ідэнтычны файлу на аддаленым вузле"), + ("identical_file_tip", "Файл ідэнтычны файлу абанента"), ("show_monitors_tip", "Паказваць маніторы на панэлі інструментаў"), ("View Mode", "Рэжым прагляду"), - ("login_linux_tip", "Каб ўключыць сеанс працоўнага стала X, неабходна ўвайсці ў аддалены акаўнт Linux."), + ("login_linux_tip", "Каб уключыць сеанс працоўнага стала X, трэба ўвайсці ў аддалены ўліковы запіс Linux."), ("verify_rustdesk_password_tip", "Пацвердзіць пароль RustDesk"), - ("remember_account_tip", "Запомніць гэты акаўнт"), - ("os_account_desk_tip", "Гэты акаўнт выкарыстоўваецца для ўваходу ў аддаленую аперацыйную сістэму і ўключэння сеансу працоўнага сталу ў рэжыме headless."), + ("remember_account_tip", "Запомніць гэты ўліковы запіс"), + ("os_account_desk_tip", "Гэты ўліковы запіс выкарыстоўваецца для ўваходу ў аддаленую аперацыйную сістэму і ўключэння сеанса працоўнага стала ў рэжыме headless."), ("OS Account", "Акаўнт АС"), - ("another_user_login_title_tip", "Іншы карыстальнік ўжо ўвайшоў у сістэму"), + ("another_user_login_title_tip", "Іншы карыстальнік ужо ўвайшоў у сістэму"), ("another_user_login_text_tip", "Адключыць"), ("xorg_not_found_title_tip", "Xorg не знойдзены"), ("xorg_not_found_text_tip", "Усталюйце Xorg"), @@ -478,39 +478,39 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("no_desktop_text_tip", "Усталюйце GNOME Desktop"), ("No need to elevate", "Павышэнне правоў не патрабуецца"), ("System Sound", "Сістэмны гук"), - ("Default", "Па змаўчанні"), + ("Default", "Стандартна"), ("New RDP", "Новы RDP"), ("Fingerprint", "Адбітак"), - ("Copy Fingerprint", "Капіраваць адбітак"), + ("Copy Fingerprint", "Капіяваць адбітак"), ("no fingerprints", "адбіткі адсутнічаюць"), - ("Select a peer", "Выберыце аддалены ўзел"), - ("Select peers", "Выберыце аддаленыя ўзлы"), - ("Plugins", "Плагіны"), + ("Select a peer", "Выберыце абанента"), + ("Select peers", "Выберыце абанентаў"), + ("Plugins", "Убудовы"), ("Uninstall", "Выдаліць"), ("Update", "Абнавіць"), ("Enable", "Уключыць"), ("Disable", "Адключыць"), ("Options", "Параметры"), - ("resolution_original_tip", "Арыгінальнае разознасць"), - ("resolution_fit_local_tip", "Супадзенне з лакальнай разрознасцю"), - ("resolution_custom_tip", "Карыстацкая разрознасць"), + ("resolution_original_tip", "Арыгінальная раздзяляльнасць"), + ("resolution_fit_local_tip", "Супадзенне з лакальнай раздзяляльнасцю"), + ("resolution_custom_tip", "Карыстацкая раздзяляльнасць"), ("Collapse toolbar", "Згарнуць панэль інструментаў"), ("Accept and Elevate", "Прыняць і павысіць"), - ("accept_and_elevate_btn_tooltip", "Дазволіць падлучэнне і павысіць правы UAC."), - ("clipboard_wait_response_timeout_tip", "Час чакання адказу капіравання буфера абмену скончыўся"), - ("Incoming connection", "Уваходнае падлучэнне"), - ("Outgoing connection", "Выходнае падлучэнне"), - ("Exit", "Выхад"), + ("accept_and_elevate_btn_tooltip", "Дазволіць падключэнне і павысіць правы UAC."), + ("clipboard_wait_response_timeout_tip", "Час чакання адказу капіявання буфера абмену скончыўся"), + ("Incoming connection", "Уваходнае падключэнне"), + ("Outgoing connection", "Выходнае падключэнне"), + ("Exit", "Выйсці"), ("Open", "Адкрыць"), - ("logout_tip", "Вы сапраўды жадаеце выйсці?"), + ("logout_tip", "Вы сапраўды хочаце выйсці?"), ("Service", "Служба"), ("Start", "Запусціць"), ("Stop", "Спыніць"), - ("exceed_max_devices", "Дасягнута максімальная колькасць кіруемых прылад."), + ("exceed_max_devices", "Дасягнута максімальная колькасць кантраляваных прылад."), ("Sync with recent sessions", "Сінхранізацыя з апошнімі сеансамі"), - ("Sort tags", "Сартаваць тэгі"), - ("Open connection in new tab", "Адкрыць падлучэнне ў новай ўкладцы"), - ("Move tab to new window", "Перамясціць ўкладку ў новае акно"), + ("Sort tags", "Сартаваць цэтлікі"), + ("Open connection in new tab", "Адкрыць падключэнне ў новай укладцы"), + ("Move tab to new window", "Перамясціць укладку ў новае акно"), ("Can not be empty", "Ня можа быць пустым"), ("Already exists", "Ужо існуе"), ("Change Password", "Змяніць пароль"), @@ -519,229 +519,229 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Grid View", "Сетка"), ("List View", "Спіс"), ("Select", "Выбар"), - ("Toggle Tags", "Пераключыць тэгі"), + ("Toggle Tags", "Пераключыць цэтлікі"), ("pull_ab_failed_tip", "Немагчыма абнавіць адрасную кнігу"), ("push_ab_failed_tip", "Немагчыма сінхранізаваць адрасную кнігу з серверам"), ("synced_peer_readded_tip", "Прылады, якія былі на апошніх сеансах, будуць сінхранізаваны з адраснай кнігай."), ("Change Color", "Змяніць колер"), ("Primary Color", "Асноўны колер"), ("HSV Color", "Колер HSV"), - ("Installation Successful!", "Інсталяцыя прайшла паспяхова!"), - ("Installation failed!", "Інсталяцыя не ўдалася!"), - ("Reverse mouse wheel", "Рэверс кола мышы"), - ("{} sessions", "{} сеансаў"), - ("scam_title", "Вы можаце быць АБМАНУТЫ!"), - ("scam_text1", "Калі вы размаўляеце па тэлефоне з кімсці, каго вы НЕ ВЕДАЕЦЕ і каму НЕ ДАВЕРАЕЦЕ, і ён просіць вас выкарыстаць RustDesk і запусціць яго службу, не працягвайце і неадкладна адмяніце размову."), - ("scam_text2", "Магчыма, гэта аферыст, які паспрабуе ўкрасць вашыя грошы або іншую асабістую інфармацыю."), + ("Installation Successful!", "Усталяванне выканана!"), + ("Installation failed!", "Усталяванне не ўдалося."), + ("Reverse mouse wheel", "Адваротнае прагортванне мышшу"), + ("{} sessions", "Колькасць сеансаў: {}"), + ("scam_title", "Вас могуць ПАДМАНУЦЬ!"), + ("scam_text1", "Калі вы размаўляеце па тэлефоне з кімсьці НЕЗНАЁМЫМ і каму вы НЕ ДАВЕРАЕЦЕ, і гэта асоба просіць вас выкарыстаць RustDesk і запусціць яго службу, не працягвайце і неадкладна скончыце размову."), + ("scam_text2", "Магчыма, гэта аферыст, які спрабуе скрасці вашы грошы або іншую асабістую інфармацыю."), ("Don't show again", "Не паказваць больш"), - ("I Agree", "Я згодны"), + ("I Agree", "Згаджаюся"), ("Decline", "Адхіліць"), ("Timeout in minutes", "Час чакання (у хвілінах)"), - ("auto_disconnect_option_tip", "Аўтаматычна зачыняць уваходныя сеансы пры неактыўнасці карыстальніка"), - ("Connection failed due to inactivity", "Падлучэнне не ўдалося з-за неактыўнасці"), + ("auto_disconnect_option_tip", "Аўтаматычна закрываць уваходныя сеансы пры неактыўнасці карыстальніка"), + ("Connection failed due to inactivity", "Збой падключэння з-за неактыўнасці"), ("Check for software update on startup", "Праверка абнаўленняў праграмы пры запуску"), - ("upgrade_rustdesk_server_pro_to_{}_tip", "Абнавіце RustDesk Server Pro да версіі {} або новейшай!"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Абнавіце RustDesk Server Pro да версіі {} або навейшай!"), ("pull_group_failed_tip", "Немагчыма абнавіць групу"), ("Filter by intersection", "Фільтраваць па перасячэнні"), - ("Remove wallpaper during incoming sessions", "Схаваць фон працоўнага стала падчас ўваходнага сеансу"), + ("Remove wallpaper during incoming sessions", "Схаваць шпалеры працоўнага стала ў часе ўваходнага сеанса"), ("Test", "Тэст"), - ("display_is_plugged_out_msg", "Дысплей адключаны, пераключыцеся на першы дысплей."), - ("No displays", "Няма дысплеяў"), + ("display_is_plugged_out_msg", "Дысплэй адключаны, пераключыцеся на першы дысплэй."), + ("No displays", "Няма дысплэяў"), ("Open in new window", "Адкрыць у новым акне"), - ("Show displays as individual windows", "Паказваць дысплеі ў асобных акнах"), - ("Use all my displays for the remote session", "Выкарыстоўваць усе мае дысплеі для аддаленага сеансу"), - ("selinux_tip", "На вашай прыладзе ўключаны SELinux, што можа перашкаджаць правільнай працы RustDesk на кіруючым баку."), - ("Change view", "Змяніць выгляд"), + ("Show displays as individual windows", "Паказваць дысплэі ў асобных вокнах"), + ("Use all my displays for the remote session", "Выкарыстоўваць усе мае дысплэі для аддаленага сеанса"), + ("selinux_tip", "На вашай прыладзе ўключаны SELinux, што можа ствараць перашкоды ў працы RustDesk на баку абанента."), + ("Change view", "Рэжым"), ("Big tiles", "Вялікія пліткі"), ("Small tiles", "Маленькія пліткі"), ("List", "Спіс"), - ("Virtual display", "Віртуальны дысплей"), + ("Virtual display", "Віртуальны дысплэй"), ("Plug out all", "Адключыць усё"), ("True color (4:4:4)", "True color (4:4:4)"), - ("Enable blocking user input", "Дазволіць блакаванне ўводу карыстальніка на прыладзе"), - ("id_input_tip", "Можна ўвесці ідэнтыфікатар, просты IP-адрас або дамен з портам (<дамен>:<порт>).\nКаб атрымаць доступ да прылады на іншым серверы, дадайце адрас сервера (@<адрас_сервера>?key=<ключ_значэнне>), напрыклад:\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nКалі неабходна атрымаць доступ да прылады на грамадскім серверы, увядзіце \"@public\", ключ для грамадскага сервера не патрабуецца."), + ("Enable blocking user input", "Дазволіць блакіраванне ўводу на прыладзе"), + ("id_input_tip", "Можна ўвесці ідэнтыфікатар, прамы IP-адрас або дамен з портам (<дамен>:<порт>).\nКаб атрымаць доступ да прылады на іншым серверы, дадайце адрас сервера (@<адрас_сервера>?key=<ключ_значэнне>), напрыклад:\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nКалі трэба атрымаць доступ да прылады на агульнадаступным серверы, увядзіце \"@public\", ключ для публічнага сервера не патрабуецца."), ("privacy_mode_impl_mag_tip", "Рэжым 1"), ("privacy_mode_impl_virtual_display_tip", "Рэжым 2"), - ("Enter privacy mode", "Уключыць рэжым канфідэнцыяльнасці"), - ("Exit privacy mode", "Адключыць рэжым канфідэнцыяльнасці"), - ("idd_not_support_under_win10_2004_tip", "Драйвер непрамога адлюстравання не падтрымліваецца. Патрабуецца Windows 10 версіі 2004 ці навейшая."), + ("Enter privacy mode", "Уключыць рэжым канфідэнцыйнасці"), + ("Exit privacy mode", "Адключыць рэжым канфідэнцыйнасці"), + ("idd_not_support_under_win10_2004_tip", "Драйвер непрамога адлюстравання не падтрымліваецца. Патрабуецца Windows 10 версіі 2004 або навейшая."), ("input_source_1_tip", "Крыніца ўводу 1"), ("input_source_2_tip", "Крыніца ўводу 2"), ("Swap control-command key", "Памяняць месцамі значэнні кнопак Ctrl і Command"), ("swap-left-right-mouse", "Памяняць месцамі значэнні левай і правай кнопак мышы"), - ("2FA code", "Код двухфактарнай аўтэнтыфікацыі"), + ("2FA code", "Код двухфактарнай праверкі сапраўднасці"), ("More", "Яшчэ"), - ("enable-2fa-title", "Выкарыстоўваць двухфактарную аўтэнтыфікацыю"), - ("enable-2fa-desc", "Наладзьце праграму аўтэнтыфікацыі. Выкарыстоўвайце, напрыклад, Authy, Microsoft або Google Authenticator на тэлефоне ці кампутары.\n\nСкануйце QR-код з дапамогай праграмы аўтэнтыфікацыі і ўвядзіце код, які пакажа гэта праграма, каб уключыць двухфактарную аўтэнтыфікацыю."), + ("enable-2fa-title", "Выкарыстоўваць двухфактарную праверку сапраўднасці"), + ("enable-2fa-desc", "Наладзьце праграму праверкі сапраўднасці. Выкарыстоўвайце, напрыклад, Authy, Microsoft або Google Authenticator на тэлефоне ці камп’ютары.\n\nАдскануйце QR-код з дапамогай праграмы праверкі сапраўднасці і ўвядзіце код, які пакажа гэта праграма, каб уключыць двухфактарную праверку сапраўднасці."), ("wrong-2fa-code", "Немагчыма пацвердзіць код. Праверце код і налады мясцовага часу."), - ("enter-2fa-title", "Двухфактарная аутэнтыфікацыя"), - ("Email verification code must be 6 characters.", "Код верыфікацыі па электроннай пошце павінен складацца з 6 сімвалаў."), - ("2FA code must be 6 digits.", "Код двухфактарнай аутэнтыфікацыі павінен складацца з 6 лічбаў."), + ("enter-2fa-title", "Двухфактарная праверка сапраўднасці"), + ("Email verification code must be 6 characters.", "Код пацвярджэння па электроннай пошце павінен складацца з 6 сімвалаў."), + ("2FA code must be 6 digits.", "Код двухфактарнай праверкі сапраўднасці павінен складацца з 6 лічбаў."), ("Multiple Windows sessions found", "Знойдзена некалькі сеансаў Windows"), - ("Please select the session you want to connect to", "Выберыце сеанс, да якога вы жадаеце падключыцца"), - ("powered_by_me", "На аснове RustDesk"), + ("Please select the session you want to connect to", "Выберыце сеанс, да якога вы хочаце падключыцца"), + ("powered_by_me", "Заснавана на RustDesk"), ("outgoing_only_desk_tip", "Гэта спецыялізаваная версія.\nВы можаце падключацца да іншых прылад, але іншыя прылады не могуць падключацца да вашай."), - ("preset_password_warning", "Гэта спецыялізаваная версія з устаноўленым загадзя паролем. Любы, хто ведае гэты пароль, можа атрымаць поўны кантроль над вашай прыладай. Калі гэта для вас нечакана, адразу выдаліце гэта праграмнае забеспячэнне."), + ("preset_password_warning", "Гэта спецыялізаваная версія з прадвызначаным паролем. Любы, хто ведае гэты пароль, можа атрымаць поўны кантроль над вашай прыладай. Калі гэта для вас нечакана, адразу выдаліце гэта праграмнае забеспячэнне."), ("Security Alert", "Папярэджанне аб бяспецы"), ("My address book", "Мая адрасная кніга"), - ("Personal", "Асабісты"), + ("Personal", "Асабістая"), ("Owner", "Уладальнік"), - ("Set shared password", "Устанавіць агульны пароль"), + ("Set shared password", "Задаць агульны пароль"), ("Exist in", "Існуе ў"), ("Read-only", "Толькі для чытання"), ("Read/Write", "Чытанне і запіс"), - ("Full Control", "Поўны кантроль"), + ("Full Control", "Поўны доступ"), ("share_warning_tip", "Палі вышэй з'яўляюцца агульнымі і бачнымі іншым."), ("Everyone", "Усе"), ("ab_web_console_tip", "Больш у вэб-кансолі"), - ("allow-only-conn-window-open-tip", "Дазволіць толькі падключэнне пры адкрытым акне RustDesk"), - ("no_need_privacy_mode_no_physical_displays_tip", "Фізічныя дысплеі адсутнічаюць, няма патрэбы выкарыстоўваць рэжым канфідэнцыяльнасці."), - ("Follow remote cursor", "Сачыць за аддаленага курсарам"), - ("Follow remote window focus", "Сачыць за фокусам аддаленага акна"), - ("default_proxy_tip", "Пратакол і порт па змаўчанні: Socks5 і 1080"), + ("allow-only-conn-window-open-tip", "Дазволіць падключэнне толькі пры адкрытым акне RustDesk"), + ("no_need_privacy_mode_no_physical_displays_tip", "Фізічныя дысплэі адсутнічаюць, няма патрэбы выкарыстоўваць рэжым канфідэнцыйнасці."), + ("Follow remote cursor", "Прытрымлівацца аддаленага курсора"), + ("Follow remote window focus", "Прытрымлівацца фокуса аддаленага акна"), + ("default_proxy_tip", "Стандартныя пратакол і порт: Socks5 і 1080"), ("no_audio_input_device_tip", "Прылада ўваходнага аудыё не знойдзена."), ("Incoming", "Уваходныя"), ("Outgoing", "Выходныя"), - ("Clear Wayland screen selection", "Адмяніць выбар экрана Wayland"), - ("clear_Wayland_screen_selection_tip", "Пасля адмены можна зноў выбраць экран для дэманстрацыі."), - ("confirm_clear_Wayland_screen_selection_tip", "Адмяніць выбар экрана Wayland?"), - ("android_new_voice_call_tip", "Атрыман новы запыт на галасавы выклік. Калі вы прымеце яго, гук пераключыцца на галасавае злучэнне."), - ("texture_render_tip", "Выкарыстоўваць візуалізацыю тэкстураў для павышэння каб плаўнасці выявы."), - ("Use texture rendering", "Візуалізацыя тэкстураў"), - ("Floating window", "Плавучае акно"), + ("Clear Wayland screen selection", "Скасаваць выбар экрана Wayland"), + ("clear_Wayland_screen_selection_tip", "Пасля скасавання можна зноў выбраць экран для дэманстрацыі."), + ("confirm_clear_Wayland_screen_selection_tip", "Скасаваць выбар экрана Wayland?"), + ("android_new_voice_call_tip", "Прыйшоў новы запыт на галасавы выклік. Калі вы прымеце яго, гук пераключыцца на галасавае падключэнне."), + ("texture_render_tip", "Выкарыстоўваць візуалізацыю тэкстур, каб зрабіць відарысы больш плаўнымі."), + ("Use texture rendering", "Візуалізацыя тэкстур"), + ("Floating window", "Нефіксаванае акно"), ("floating_window_tip", "Дапамагае падтрымліваць фонавую службу RustDesk"), ("Keep screen on", "Трымаць экран уключаным"), ("Never", "Ніколі"), ("During controlled", "Пры кіраванні"), ("During service is on", "Пры запушчанай службе"), ("Capture screen using DirectX", "Захоп экрана з выкарыстаннем DirectX"), - ("Back", ""), - ("Apps", ""), - ("Volume up", ""), - ("Volume down", ""), - ("Power", ""), - ("Telegram bot", ""), - ("enable-bot-tip", ""), - ("enable-bot-desc", ""), - ("cancel-2fa-confirm-tip", ""), - ("cancel-bot-confirm-tip", ""), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), - ("one-way-file-transfer-tip", ""), - ("Authentication Required", ""), - ("Authenticate", ""), - ("web_id_input_tip", ""), - ("Download", ""), - ("Upload folder", ""), - ("Upload files", ""), - ("Clipboard is synchronized", ""), - ("Update client clipboard", ""), - ("Untagged", ""), - ("new-version-of-{}-tip", ""), - ("Accessible devices", ""), - ("upgrade_remote_rustdesk_client_to_{}_tip", "Калі ласка, абнавіце кліент RustDesk да версіі {} або навейшай на аддаленым баку!"), - ("d3d_render_tip", ""), - ("Use D3D rendering", ""), - ("Printer", ""), - ("printer-os-requirement-tip", ""), - ("printer-requires-installed-{}-client-tip", ""), - ("printer-{}-not-installed-tip", ""), - ("printer-{}-ready-tip", ""), - ("Install {} Printer", ""), - ("Outgoing Print Jobs", ""), - ("Incoming Print Jobs", ""), - ("Incoming Print Job", ""), - ("use-the-default-printer-tip", ""), - ("use-the-selected-printer-tip", ""), - ("auto-print-tip", ""), - ("print-incoming-job-confirm-tip", ""), - ("remote-printing-disallowed-tile-tip", ""), - ("remote-printing-disallowed-text-tip", ""), - ("save-settings-tip", ""), - ("dont-show-again-tip", ""), - ("Take screenshot", ""), - ("Taking screenshot", ""), - ("screenshot-merged-screen-not-supported-tip", ""), - ("screenshot-action-tip", ""), - ("Save as", ""), - ("Copy to clipboard", ""), - ("Enable remote printer", ""), - ("Downloading {}", ""), - ("{} Update", ""), - ("{}-to-update-tip", ""), - ("download-new-version-failed-tip", ""), - ("Auto update", ""), - ("update-failed-check-msi-tip", ""), - ("websocket_tip", ""), - ("Use WebSocket", ""), - ("Trackpad speed", ""), - ("Default trackpad speed", ""), - ("Numeric one-time password", ""), - ("Enable IPv6 P2P connection", ""), - ("Enable UDP hole punching", ""), - ("View camera", "Прагляд камеры"), - ("Enable camera", ""), - ("No cameras", ""), - ("view_camera_unsupported_tip", ""), - ("Terminal", ""), - ("Enable terminal", ""), - ("New tab", ""), - ("Keep terminal sessions on disconnect", ""), - ("Terminal (Run as administrator)", ""), - ("terminal-admin-login-tip", ""), - ("Failed to get user token.", ""), - ("Incorrect username or password.", ""), - ("The user is not an administrator.", ""), - ("Failed to check if the user is an administrator.", ""), - ("Supported only in the installed version.", ""), - ("elevation_username_tip", ""), - ("Preparing for installation ...", ""), - ("Show my cursor", ""), - ("Scale custom", ""), - ("Custom scale slider", ""), - ("Decrease", ""), - ("Increase", ""), - ("Show virtual mouse", ""), - ("Virtual mouse size", ""), - ("Small", ""), - ("Large", ""), - ("Show virtual joystick", ""), - ("Edit note", ""), - ("Alias", ""), - ("ScrollEdge", ""), - ("Allow insecure TLS fallback", ""), - ("allow-insecure-tls-fallback-tip", ""), - ("Disable UDP", ""), - ("disable-udp-tip", ""), - ("server-oss-not-support-tip", ""), - ("input note here", ""), - ("note-at-conn-end-tip", ""), - ("Show terminal extra keys", ""), - ("Relative mouse mode", ""), - ("rel-mouse-not-supported-peer-tip", ""), - ("rel-mouse-not-ready-tip", ""), - ("rel-mouse-lock-failed-tip", ""), - ("rel-mouse-exit-{}-tip", ""), - ("rel-mouse-permission-lost-tip", ""), - ("Changelog", ""), - ("keep-awake-during-outgoing-sessions-label", ""), - ("keep-awake-during-incoming-sessions-label", ""), + ("Back", "Назад"), + ("Apps", "Праграмы"), + ("Volume up", "Гучнасць+"), + ("Volume down", "Гучнасць-"), + ("Power", "Сілкаванне"), + ("Telegram bot", "Telegram-бот"), + ("enable-bot-tip", "Калі ўключана, можна атрымліваць код двухфактарнай праверкі сапраўднасці ад бота. Таксама ён можа выконваць функцыю апавяшчэння пра падключэнне."), + ("enable-bot-desc", "1) Адкрыйце чат з @BotFather.\n2) Адпраўце каманду \"/newbot\". Пасля выканання гэтага кроку вы атрымаеце токен.\n3) Пачніце чат з вашым толькі што створаным ботам. Адпраўце паведамленне, якое пачынаецца з касой рысы (\"/\"), напрыклад, \"/hello\", каб яго актываваць.\n"), + ("cancel-2fa-confirm-tip", "Адключыць двухфактарную праверку сапраўднасці?"), + ("cancel-bot-confirm-tip", "Адключыць Telegram-бота"), + ("About RustDesk", "Пра RustDesk"), + ("Send clipboard keystrokes", "Адпраўляць націсканні клавіш у буфер абмену"), + ("network_error_tip", "Праверце падключэнне да сеткі, пасля чаго націсніце \"Паўтарыць спробу\"."), + ("Unlock with PIN", "Разблакіраваць PIN-кодам"), + ("Requires at least {} characters", "Патрабуецца больш сімвалаў (ад {})"), + ("Wrong PIN", "Памылковы PIN-код"), + ("Set PIN", "Задаць PIN-код"), + ("Enable trusted devices", "Уключэнне давераных прылад"), + ("Manage trusted devices", "Кіраванне даверанымі прыладамі"), + ("Platform", "Платформа"), + ("Days remaining", "Засталося дзён"), + ("enable-trusted-devices-tip", "Дазволіць давераным прыладам прапускаць праверку сапраўднасці 2FA"), + ("Parent directory", "Бацькоўскі каталог"), + ("Resume", "Працягнуць"), + ("Invalid file name", "Памылковая назва файла"), + ("one-way-file-transfer-tip", "На баку абанента ўключана аднабаковая перадача файлаў."), + ("Authentication Required", "Патрабуецца праверка сапраўднасці"), + ("Authenticate", "Прайсці праверку"), + ("web_id_input_tip", "Можна ўвесці ID на тым самым серверы, прамы доступ па IP у вэб-кліенце не падтрымліваецца.\nКалі вы хочаце атрымаць доступ да прылады на іншым серверы, дадайце адрас сервера (@<адрас_сервера>?key=<ключ>), напрыклад,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nКалі вы хочаце атрымаць доступ да прылады на публічным серверы, увядзіце \"@public\", для публічнага сервера ключ не патрэбны."), + ("Download", "Спампаваць"), + ("Upload folder", "Запампаваць папку"), + ("Upload files", "Запампаваць файлы"), + ("Clipboard is synchronized", "Буфер абмену сінхранізаваны"), + ("Update client clipboard", "Абнавіць буфер абмену кліента"), + ("Untagged", "Без цэтліка"), + ("new-version-of-{}-tip", "Даступна новая версія {}"), + ("Accessible devices", "Даступныя прылады"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Абнавіце кліент RustDesk да версіі {} або навейшай на баку абанента!"), + ("d3d_render_tip", "Пры ўключэнні візуалізацыі D3D на некаторых прыладах аддалены экран можа быць чорным."), + ("Use D3D rendering", "Выкарыстоўваць візуалізацыю D3D"), + ("Printer", "Прынтар"), + ("printer-os-requirement-tip", "Для работы функцыі выходнай сувязі з прынтарам патрабуецца Windows 10 або навейшай версіі."), + ("printer-requires-installed-{}-client-tip", "Каб выкарыстоўваць аддалены друк, {} павінен быць усталяваны на гэтай прыладзе."), + ("printer-{}-not-installed-tip", "Прынтар {} не ўсталяваны."), + ("printer-{}-ready-tip", "Прынтар {} усталяваны і гатовы да выкарыстання."), + ("Install {} Printer", "Усталюйце прынтар {}"), + ("Outgoing Print Jobs", "Выходныя заданні друку"), + ("Incoming Print Jobs", "Уваходныя заданні друку"), + ("Incoming Print Job", "Уваходнае заданне друку"), + ("use-the-default-printer-tip", "Выкарыстоўваць прынтар стандартна"), + ("use-the-selected-printer-tip", "Выкарыстоўваць выбраны прынтар"), + ("auto-print-tip", "Аўтаматычна выконваць друк на выбраным прынтары"), + ("print-incoming-job-confirm-tip", "З аддаленай прылады атрымана заданне на друк. Выканаць яго лакальна?"), + ("remote-printing-disallowed-tile-tip", "Аддалены друк забаронены"), + ("remote-printing-disallowed-text-tip", "Налады дазволаў на баку абанента забараняюць аддалены друк."), + ("save-settings-tip", "Захаваць налады"), + ("dont-show-again-tip", "Больш не паказваць"), + ("Take screenshot", "Зрабіць здымак экрана"), + ("Taking screenshot", "Робіцца здымак экрана"), + ("screenshot-merged-screen-not-supported-tip", "Аб’яднанне здымкаў экранаў з некалькіх дысплэяў у дадзены момант не падтрымліваецца. Пераключыцеся на адзін з дысплэяў і паўтарыце дзеянне."), + ("screenshot-action-tip", "Выберыце, што рабіць з атрыманым здымкам экрана."), + ("Save as", "Захаваць у файл"), + ("Copy to clipboard", "Скапіяваць у буфер абмену"), + ("Enable remote printer", "Выкарыстоўваць аддалены прынтар"), + ("Downloading {}", "Ідзе спампоўванне {}"), + ("{} Update", "Абнавіць {}"), + ("{}-to-update-tip", "{} закрыецца і ўсталюе новую версію."), + ("download-new-version-failed-tip", "Памылка спампоўвання. Можна паўтарыць спробу або націснуць кнопку \"Спампаваць\", каб спампаваць праграму з афіцыйнага сайта і абнавіць уручную."), + ("Auto update", "Аўтаматычнае абнаўленне"), + ("update-failed-check-msi-tip", "Немагчыма вызначыць метад усталявання. Націсніце кнопку \"Спампаваць\", каб спампаваць праграму з афіцыйнага сайта і абнавіце яго ўручную."), + ("websocket_tip", "WebSocket падтрымлівае толькі падключэнні да рэтранслятара."), + ("Use WebSocket", "Выкарыстоўваць WebSocket"), + ("Trackpad speed", "Хуткасць трэкпада"), + ("Default trackpad speed", "Стандартная хуткасць трэкпада"), + ("Numeric one-time password", "Лічбавы аднаразовы пароль"), + ("Enable IPv6 P2P connection", "Выкарыстоўваць падключэнне IPv6 P2P"), + ("Enable UDP hole punching", "Выкарыстоўваць UDP hole punching"), + ("View camera", "Рэжым камеры"), + ("Enable camera", "Уключыць камеру"), + ("No cameras", "Камера адсутнічае"), + ("view_camera_unsupported_tip", "Аддаленая прылада не падтрымлівае рэжыму камеры."), + ("Terminal", "Тэрмінал"), + ("Enable terminal", "Уключыць тэрмінал"), + ("New tab", "Новая ўкладка"), + ("Keep terminal sessions on disconnect", "Захоўваць сеансы тэрмінала пры адключэнні"), + ("Terminal (Run as administrator)", "Тэрмінал (адміністратар)"), + ("terminal-admin-login-tip", "Увядзіце імя карыстальніка і пароль адміністратара абанента."), + ("Failed to get user token.", "Не ўдалося атрымаць токен карыстальніка."), + ("Incorrect username or password.", "Памылковае імя карыстальніка або пароль."), + ("The user is not an administrator.", "Карыстальнік не з’яўляецца адміністратарам."), + ("Failed to check if the user is an administrator.", "Немагчыма праверыць, ці з’яўляецца карыстальнік адміністратарам."), + ("Supported only in the installed version.", "Падтрымліваецца толькі ва ўсталёвачнай версіі."), + ("elevation_username_tip", "Увядзіце карыстальніка або дамен\\карыстальніка"), + ("Preparing for installation ...", "Ідзе падрыхтоўка да ўсталявання..."), + ("Show my cursor", "Паказваць мой курсор"), + ("Scale custom", "Карыстальніцкае маштабаванне"), + ("Custom scale slider", "Карыстальніцкі паўзунок маштабавання"), + ("Decrease", "Паменшыць"), + ("Increase", "Павялічыць"), + ("Show virtual mouse", "Паказаць віртуальную мыш"), + ("Virtual mouse size", "Памер віртуальнай мышы"), + ("Small", "Маленькі"), + ("Large", "Вялікі"), + ("Show virtual joystick", "Паказваць віртуальны джойстык"), + ("Edit note", "Змяніць нататку"), + ("Alias", "Псеўданім"), + ("ScrollEdge", "Прагортваць з краю"), + ("Allow insecure TLS fallback", "Дазволіць небяспечныя TLS"), + ("allow-insecure-tls-fallback-tip", "Стандартна RustDesk правярае сертыфікат сервера на наяўнасць пратаколаў, якія выкарыстоўваюць TLS.\nКалі гэта функцыя ўключана, RustDesk прапусціць дадзены этап і працягне працу ў выпадку няўдалай праверкі."), + ("Disable UDP", "Выключыць UDP"), + ("disable-udp-tip", "Вызначае, ці варта выкарыстоўваць толькі TCP.\nКалі ўключана, RustDesk не будзе выкарыстоўваць UDP 21116, замест чаго будзе выкарыстоўвацца TCP 21116."), + ("server-oss-not-support-tip", "ЗАЎВАГА! у OSS-серверы RustDesk гэта функцыя адсутнічае."), + ("input note here", "увядзіце нататку"), + ("note-at-conn-end-tip", "Запытваць нататку ў канцы сеанса"), + ("Show terminal extra keys", "Паказваць дадатковыя кнопкі тэрмінала"), + ("Relative mouse mode", "Рэжым адноснага перамяшчэння мышы"), + ("rel-mouse-not-supported-peer-tip", "Рэжым адноснага перамяшчэння мышы не падтрымліваецца падключаным абанентам."), + ("rel-mouse-not-ready-tip", "Рэжым адноснага перамяшчэння мышы яшчэ не гатовы. Паспрабуйце зноў."), + ("rel-mouse-lock-failed-tip", "Немагчыма заблакіраваць курсор. Рэжым адноснага перамяшчэння мышы адключаны."), + ("rel-mouse-exit-{}-tip", "Націсніце {}, каб выйсці."), + ("rel-mouse-permission-lost-tip", "Дазвол на выкарыстанне клавіятуры скасаваны. Рэжым адноснага перамяшчэння мышы адключаны."), + ("Changelog", "Журнал змяненняў"), + ("keep-awake-during-outgoing-sessions-label", "Не адключаць экрана ў часе выходных сеансаў"), + ("keep-awake-during-incoming-sessions-label", "Не адключаць экрана ў часе ўваходных сеансаў"), ("Continue with {}", "Працягнуць з {}"), - ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), + ("Display Name", "Імя для адлюстравання"), + ("password-hidden-tip", "Зададзены пастаянны пароль (скрыты)."), + ("preset-password-in-use-tip", "Пададзены пароль цяпер выкарыстоўваецца"), ].iter().cloned().collect(); } From 6cb323725b5cbf71bc0ed514703e1bd187b9aa32 Mon Sep 17 00:00:00 2001 From: fufesou <13586388+fufesou@users.noreply.github.com> Date: Fri, 24 Apr 2026 14:35:58 +0800 Subject: [PATCH 230/277] fix(sicter): control side, privacy mode (#14880) Signed-off-by: fufesou --- src/ui/header.tis | 10 ++++++++-- src/ui/remote.rs | 17 +++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/ui/header.tis b/src/ui/header.tis index 17efe6982..2698ce4d0 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -602,7 +602,13 @@ function togglePrivacyMode(privacy_id) { if (!supported) { msgbox("nocancel", translate("Privacy mode"), translate("Unsupported"), "", function() { }); } else { - handler.toggle_option(privacy_id); + var privacy_mode_impls = pi.platform_additions?.supported_privacy_mode_impl; + if (privacy_mode_impls == null || privacy_mode_impls == undefined) { + handler.toggle_option(privacy_id); + return; + } + var is_on = handler.get_toggle_option("privacy-mode"); + handler.toggle_privacy_mode("", !is_on); } } @@ -713,4 +719,4 @@ handler.setConnectionType = function(secured, direct, stream_type) { handler.updateRecordStatus = function(status) { recording = status; header.update(); -} \ No newline at end of file +} diff --git a/src/ui/remote.rs b/src/ui/remote.rs index a575cf397..8b6f01ae0 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -85,6 +85,22 @@ impl SciterHandler { serde_json::Value::Bool(b) => { value.set_item(k, b); } + serde_json::Value::Array(arr) if k == "supported_privacy_mode_impl" => { + let mut impls = Value::array(0); + for item in arr { + if let serde_json::Value::Array(entry) = item { + let impl_key = entry.get(0).and_then(|v| v.as_str()); + let impl_name = entry.get(1).and_then(|v| v.as_str()); + if let (Some(impl_key), Some(impl_name)) = (impl_key, impl_name) { + let mut impl_item = Value::array(0); + impl_item.push(impl_key); + impl_item.push(impl_name); + impls.push(impl_item); + } + } + } + value.set_item(k, impls); + } _ => { // ignore for now } @@ -550,6 +566,7 @@ impl sciter::EventHandler for SciterSession { fn get_toggle_option(String); fn is_privacy_mode_supported(); fn toggle_option(String); + fn toggle_privacy_mode(String, bool); fn get_remember(); fn peer_platform(); fn set_write_override(i32, i32, bool, bool, bool); From 03e351ac61255eba956155ff84c7a6d238ebea42 Mon Sep 17 00:00:00 2001 From: Nawer Date: Fri, 24 Apr 2026 12:38:34 +0200 Subject: [PATCH 231/277] feat(i18n): Complete and fix french translations (#14890) --- docs/CODE_OF_CONDUCT-FR.md | 143 +++++++++++++++++++++++++++++++++++++ docs/CONTRIBUTING-FR.md | 55 ++++++++++++++ docs/README-FR.md | 6 +- docs/SECURITY-FR.md | 16 +++++ src/lang/fr.rs | 4 +- 5 files changed, 219 insertions(+), 5 deletions(-) create mode 100644 docs/CODE_OF_CONDUCT-FR.md create mode 100644 docs/CONTRIBUTING-FR.md create mode 100644 docs/SECURITY-FR.md diff --git a/docs/CODE_OF_CONDUCT-FR.md b/docs/CODE_OF_CONDUCT-FR.md new file mode 100644 index 000000000..dca61e0aa --- /dev/null +++ b/docs/CODE_OF_CONDUCT-FR.md @@ -0,0 +1,143 @@ + +# Code de conduite des contributeurs + +## Notre engagement + +En tant que membres, contributeurs et responsables, nous nous engageons à faire +de la participation à notre communauté une expérience exempte de harcèlement pour +tous, indépendamment de l'âge, de la taille corporelle, du handicap visible ou +invisible, de l'origine ethnique, des caractéristiques sexuelles, de l'identité +et de l'expression de genre, du niveau d'expérience, de l'éducation, du statut +socio-économique, de la nationalité, de l'apparence personnelle, de la race, de +la religion ou de l'identité et de l'orientation sexuelle. + +Nous nous engageons à agir et à interagir de manière à contribuer à une +communauté ouverte, accueillante, diversifiée, inclusive et saine. + +## Nos standards + +Exemples de comportements qui contribuent à un environnement positif pour notre +communauté : + +* Faire preuve d'empathie et de bienveillance envers les autres +* Respecter les opinions, les points de vue et les expériences différents +* Donner et accepter gracieusement les retours constructifs +* Assumer ses responsabilités, s'excuser auprès des personnes affectées par nos + erreurs et apprendre de l'expérience +* Se concentrer sur ce qui est le mieux non seulement pour nous en tant + qu'individus, mais pour l'ensemble de la communauté + +Exemples de comportements inacceptables : + +* L'utilisation de langage ou d'images à caractère sexuel, et les attentions ou + avances sexuelles de quelque nature que ce soit +* Le trolling, les commentaires insultants ou désobligeants, et les attaques + personnelles ou politiques +* Le harcèlement public ou privé +* La publication d'informations privées d'autrui, telles qu'une adresse physique + ou électronique, sans autorisation explicite +* Tout autre comportement qui pourrait raisonnablement être considéré comme + inapproprié dans un cadre professionnel + +## Responsabilités en matière d'application + +Les responsables de la communauté sont chargés de clarifier et d'appliquer nos +standards de comportement acceptable et prendront des mesures correctives +appropriées et équitables en réponse à tout comportement qu'ils jugent +inapproprié, menaçant, offensant ou nuisible. + +Les responsables de la communauté ont le droit et la responsabilité de +supprimer, modifier ou rejeter les commentaires, commits, code, modifications +du wiki, issues et autres contributions qui ne sont pas conformes à ce Code de +conduite, et communiqueront les raisons de leurs décisions de modération le cas +échéant. + +## Portée + +Ce Code de conduite s'applique dans tous les espaces communautaires, et +s'applique également lorsqu'une personne représente officiellement la communauté +dans les espaces publics. Les exemples de représentation de notre communauté +incluent l'utilisation d'une adresse e-mail officielle, la publication via un +compte de réseau social officiel, ou le fait d'agir en tant que représentant +désigné lors d'un événement en ligne ou hors ligne. + +## Application + +Les cas de comportements abusifs, harcelants ou autrement inacceptables peuvent +être signalés aux responsables de la communauté chargés de l'application à +[info@rustdesk.com](mailto:info@rustdesk.com). +Toutes les plaintes seront examinées et feront l'objet d'une enquête rapide et +équitable. + +Tous les responsables de la communauté sont tenus de respecter la vie privée et +la sécurité de la personne ayant signalé un incident. + +## Directives d'application + +Les responsables de la communauté suivront ces Directives d'impact communautaire +pour déterminer les conséquences de toute action qu'ils jugent en violation de ce +Code de conduite : + +### 1. Correction + +**Impact communautaire** : Utilisation d'un langage inapproprié ou autre +comportement jugé non professionnel ou indésirable dans la communauté. + +**Conséquence** : Un avertissement écrit et privé de la part des responsables de +la communauté, expliquant la nature de la violation et pourquoi le comportement +était inapproprié. Des excuses publiques peuvent être demandées. + +### 2. Avertissement + +**Impact communautaire** : Une violation par un incident isolé ou une série +d'actions. + +**Conséquence** : Un avertissement avec des conséquences en cas de comportement +répété. Aucune interaction avec les personnes impliquées, y compris les +interactions non sollicitées avec les personnes chargées d'appliquer le Code de +conduite, pendant une période déterminée. Cela inclut d'éviter les interactions +dans les espaces communautaires ainsi que dans les canaux externes comme les +réseaux sociaux. Le non-respect de ces conditions peut entraîner une exclusion +temporaire ou permanente. + +### 3. Exclusion temporaire + +**Impact communautaire** : Une violation grave des standards communautaires, y +compris un comportement inapproprié persistant. + +**Conséquence** : Une exclusion temporaire de toute interaction ou communication +publique avec la communauté pendant une période déterminée. Aucune interaction +publique ou privée avec les personnes impliquées, y compris les interactions non +sollicitées avec les personnes chargées d'appliquer le Code de conduite, n'est +autorisée pendant cette période. Le non-respect de ces conditions peut entraîner +une exclusion permanente. + +### 4. Exclusion permanente + +**Impact communautaire** : Démontrer un schéma de violation des standards +communautaires, y compris un comportement inapproprié persistant, le harcèlement +d'une personne, ou une agression envers des catégories de personnes ou leur +dénigrement. + +**Conséquence** : Une exclusion permanente de toute interaction publique au sein +de la communauté. + +## Attribution + +Ce Code de conduite est adapté du [Contributor Covenant][homepage], version 2.0, +disponible à l'adresse +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. + +Les Directives d'impact communautaire ont été inspirées par +[l'échelle d'application du code de conduite de Mozilla][Mozilla CoC]. + +Pour des réponses aux questions fréquentes sur ce code de conduite, consultez la +FAQ à l'adresse [https://www.contributor-covenant.org/faq][FAQ]. Des traductions +sont disponibles à l'adresse +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/docs/CONTRIBUTING-FR.md b/docs/CONTRIBUTING-FR.md new file mode 100644 index 000000000..6f800de7d --- /dev/null +++ b/docs/CONTRIBUTING-FR.md @@ -0,0 +1,55 @@ + +# Contribuer à RustDesk + +RustDesk accueille les contributions de tous. Voici les directives si vous +envisagez de nous aider : + +## Contributions + +Les contributions à RustDesk ou à ses dépendances doivent être soumises sous +forme de pull requests GitHub. Chaque pull request sera examinée par un +contributeur principal (une personne ayant la permission d'intégrer des +correctifs) et sera soit intégrée dans la branche principale, soit accompagnée +de retours sur les modifications requises. Toutes les contributions doivent +suivre ce format, même celles des contributeurs principaux. + +Si vous souhaitez travailler sur une issue, veuillez d'abord la revendiquer en +commentant sur l'issue GitHub indiquant que vous souhaitez la traiter. Cela +permet d'éviter les efforts en double de la part des contributeurs sur la même +issue. + +## Liste de vérification pour les pull requests + +- Partez de la branche master et, si nécessaire, effectuez un rebase sur la + branche master actuelle avant de soumettre votre pull request. Si elle ne + fusionne pas proprement avec master, il vous sera peut-être demandé de + rebaser vos modifications. + +- Les commits doivent être aussi petits que possible, tout en s'assurant que + chaque commit est correct de manière indépendante (c.-à-d. que chaque commit + doit compiler et passer les tests). + +- Les commits doivent être accompagnés d'une signature Developer Certificate of + Origin (http://developercertificate.org), indiquant que vous (et votre + employeur le cas échéant) acceptez d'être liés par les termes de la + [licence du projet](../LICENCE). Dans git, il s'agit de l'option `-s` de + `git commit`. + +- Si votre correctif n'est pas examiné ou si vous avez besoin qu'une personne + spécifique l'examine, vous pouvez @-mentionner un relecteur pour demander une + revue dans la pull request ou un commentaire, ou vous pouvez demander une + revue par [e-mail](mailto:info@rustdesk.com). + +- Ajoutez des tests relatifs au bug corrigé ou à la nouvelle fonctionnalité. + +Pour des instructions git spécifiques, consultez le +[GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow). + +## Conduite + +https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md + +## Communication + +Les contributeurs de RustDesk se retrouvent fréquemment sur +[Discord](https://discord.gg/nDceKgxnkV). diff --git a/docs/README-FR.md b/docs/README-FR.md index c2e25886d..345e53b58 100644 --- a/docs/README-FR.md +++ b/docs/README-FR.md @@ -34,9 +34,9 @@ Les versions de bureau utilisent [sciter](https://sciter.com/) pour l'interface - Installez [vcpkg](https://github.com/microsoft/vcpkg), et définissez correctement la variable d'environnement `VCPKG_ROOT`. - Windows : vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static - - Linux/Osx : vcpkg install libvpx libyuv opus aom + - Linux/macOS : vcpkg install libvpx libyuv opus aom -- Exécuter `cargo run` +- Exécutez `cargo run` ## Comment compiler/build sous Linux @@ -93,7 +93,7 @@ cd rustdesk mkdir -p target/debug wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so mv libsciter-gtk.so target/debug -Exécution du cargo +cargo run ``` ## Comment construire avec Docker diff --git a/docs/SECURITY-FR.md b/docs/SECURITY-FR.md new file mode 100644 index 000000000..1cf2c6167 --- /dev/null +++ b/docs/SECURITY-FR.md @@ -0,0 +1,16 @@ + +# Politique de sécurité + +## Signaler une vulnérabilité + +Nous accordons une très grande importance à la sécurité du projet. Nous +encourageons tous les utilisateurs à nous signaler toute vulnérabilité qu'ils +découvrent. + +Si vous trouvez une vulnérabilité de sécurité dans le projet RustDesk, veuillez +la signaler de manière responsable en envoyant un e-mail à info@rustdesk.com. + +À ce stade, nous n'avons pas de programme de bug bounty. Nous sommes une petite +équipe qui s'attaque à un grand défi. Nous vous encourageons vivement à signaler +toute vulnérabilité de manière responsable afin que nous puissions continuer à +développer une application sécurisée pour l'ensemble de la communauté. diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 0dda7817f..8ad712f1e 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -741,7 +741,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Maintenir l’écran allumé lors des sessions entrantes"), ("Continue with {}", "Continuer avec {}"), ("Display Name", "Nom d’affichage"), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), + ("password-hidden-tip", "Le mot de passe permanent est défini (masqué)."), + ("preset-password-in-use-tip", "Le mot de passe prédéfini est actuellement utilisé."), ].iter().cloned().collect(); } From 38f13007171f395e36a5730e70746a68620a97f7 Mon Sep 17 00:00:00 2001 From: Sergiusz Michalik Date: Sat, 25 Apr 2026 06:46:05 +0200 Subject: [PATCH 232/277] fix(linux): enable mouse side buttons in remote sessions (#14848) * fix(linux): enable mouse side buttons in remote sessions Flutter's Linux embedder never delivers X11 button 8/9 (back/forward) events to Dart, so mouse side buttons were silently dropped in remote sessions. Intercept these buttons at the GDK level via button-press/release-event handlers on all windows (main + sub-windows) and forward them through a dedicated platform channel to the active InputModel session. Also add a defensive XSetPointerMapping call during enigo init to extend the X11 core pointer button map to 9 buttons on servers where it is smaller (e.g. minimal X server configurations). * fix: address review feedback for side button support - Use XOpenDisplay/XCloseDisplay instead of reading Display* from xdo_t's private struct layout at offset 0 (fragile ABI assumption) - Track side button down ownership per button via a Map instead of a single slot, preventing cross-button mismatch on overlapping presses * fix: gate side buttons on view-only and fix teardown - Skip side button events in view-only sessions (consistent with other mouse entry points) - Release held side buttons on session close to avoid stuck buttons on the remote - Drop unpaired 'up' events instead of falling back to the active model, which could send to the wrong session * docs: add clarifying comments from review feedback - Note global scope of XSetPointerMapping and that it runs once via lazy_static singleton - Clarify sub-window callback is safe on X11-only builds - Document per-isolate design of initSideButtonChannel * fix: replace broken XSetPointerMapping with diagnostic check XSetPointerMapping requires the length to match XGetPointerMapping's return value - it cannot extend the button count. The previous code would trigger a BadValue X error on servers with fewer than 9 buttons. Replace with a diagnostic-only check that logs whether the core pointer has enough buttons for side button simulation. RustDesk's uinput "Mouse passthrough" device already provides the needed buttons in practice. Also add .catchError to fire-and-forget side button releases during session teardown to prevent unhandled async errors. * fix: ensure side button releases bypass permission checks If permissions change between button down and up (e.g. keyboardPerm revoked, view-only toggled), sendMouse's early return would suppress the release, leaving a stuck button on the remote. Add _sendMouseUnchecked that bypasses permission checks, used for: - Side button 'up' events (matching a recorded 'down') - Forced releases during session teardown Gate all permission checks (isViewOnly, keyboardPerm, isViewCamera) at the 'down' entry point before recording in _sideButtonDownModels. * fix: add NULL guards and avoid blocking platform channel handler - Add NULL checks for FL_VIEW cast and channel creation in on_subwindow_created (review feedback from fufesou) - Use fire-and-forget (unawaited) for _sendMouseUnchecked calls inside the platform channel handler to avoid blocking platform messages when sessionSendMouse is slow (review feedback from Copilot) * fix: remove circular import and skip X11 check on Wayland - Move initSideButtonChannel() call from initEnv() in main.dart to the InputModel constructor, removing the circular import between main.dart and input_model.dart - Skip check_x11_button_map() when DISPLAY is not set to avoid noisy warnings on pure Wayland environments --- flutter/lib/models/input_model.dart | 99 +++++++++++++++++++++++++++-- flutter/lib/models/model.dart | 1 + flutter/linux/my_application.cc | 89 ++++++++++++++++++++++++-- libs/enigo/src/linux/xdo.rs | 47 ++++++++++++++ 4 files changed, 227 insertions(+), 9 deletions(-) diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 675a95e42..ab9278217 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -20,7 +20,7 @@ import '../common.dart'; import '../consts.dart'; /// Mouse button enum. -enum MouseButtons { left, right, wheel, back } +enum MouseButtons { left, right, wheel, back, forward } const _kMouseEventDown = 'mousedown'; const _kMouseEventUp = 'mouseup'; @@ -157,6 +157,8 @@ extension ToString on MouseButtons { return 'wheel'; case MouseButtons.back: return 'back'; + case MouseButtons.forward: + return 'forward'; } } } @@ -327,6 +329,80 @@ class ToReleaseKeys { } class InputModel { + // Side mouse button support for Linux. + // Flutter's Linux embedder drops X11 button 8/9 events, so we capture them + // natively via GDK and forward through the platform channel. + static InputModel? _activeSideButtonModel; + // Tracks per-button which model received a side button down event, so the + // matching up event is routed there even if the pointer has left the view + // or a different button was pressed in between. + static final Map _sideButtonDownModels = {}; + static bool _sideButtonChannelInitialized = false; + + /// Each Flutter engine (main window + sub-windows from desktop_multi_window) + /// runs its own Dart isolate with its own statics. Called from initEnv() + /// which runs per-engine, so each isolate registers its own handler tied + /// to its own set of InputModels. + static void initSideButtonChannel() { + if (!Platform.isLinux) return; + if (_sideButtonChannelInitialized) return; + _sideButtonChannelInitialized = true; + + const channel = MethodChannel('org.rustdesk.rustdesk/side_buttons'); + channel.setMethodCallHandler((call) async { + if (call.method == 'onSideMouseButton') { + final args = call.arguments as Map; + final button = args['button'] as String; + final type = args['type'] as String; + final mb = button == 'back' ? MouseButtons.back : MouseButtons.forward; + + if (type == 'down') { + final model = _activeSideButtonModel; + if (model != null && + !(model.isViewOnly && !model.showMyCursor) && + model.keyboardPerm && + !model.isViewCamera) { + _sideButtonDownModels[mb] = model; + // Fire-and-forget to avoid blocking the platform channel handler. + unawaited(model._sendMouseUnchecked(type, mb).catchError((Object e) { + debugPrint('[InputModel] failed to send side button $type for $mb: $e'); + })); + } + } else { + // Only route 'up' when we recorded the matching 'down'; + // dropping avoids sending unpaired 'up' to an unrelated session. + // Use _sendMouseUnchecked to bypass permission checks so the + // release always goes through even if permissions changed. + final model = _sideButtonDownModels.remove(mb); + if (model != null) { + unawaited(model._sendMouseUnchecked(type, mb).catchError((Object e) { + debugPrint('[InputModel] failed to send side button $type for $mb: $e'); + })); + } + } + } + return null; + }); + } + + /// Clear any static references to this model (prevents stale routing). + /// Releases any held side buttons on the peer so closing a session + /// mid-press does not leave a stuck button. + void disposeSideButtonTracking() { + if (_activeSideButtonModel == this) _activeSideButtonModel = null; + final held = _sideButtonDownModels.entries + .where((e) => e.value == this) + .map((e) => e.key) + .toList(); + for (final mb in held) { + _sideButtonDownModels.remove(mb); + // Best-effort release; session may already be tearing down. + unawaited(_sendMouseUnchecked('up', mb).catchError((Object e) { + debugPrint('[InputModel] failed to release side button $mb: $e'); + })); + } + } + final WeakReference parent; String keyboardMode = ''; @@ -412,6 +488,7 @@ class InputModel { bool get isRelativeMouseModeSupported => _relativeMouse.isSupported; InputModel(this.parent) { + initSideButtonChannel(); sessionId = parent.target!.sessionId; _relativeMouse = RelativeMouseModel( sessionId: sessionId, @@ -966,13 +1043,20 @@ class InputModel { return evt; } + /// Send mouse event unconditionally (no permission checks). + /// Used for side button releases that must go through even if permissions + /// changed after the matching down was sent. + Future _sendMouseUnchecked(String type, MouseButtons button) async { + await bind.sessionSendMouse( + sessionId: sessionId, + msg: json.encode(modify({'type': type, 'buttons': button.value}))); + } + /// Send mouse press event. Future sendMouse(String type, MouseButtons button) async { if (!keyboardPerm) return; if (isViewCamera) return; - await bind.sessionSendMouse( - sessionId: sessionId, - msg: json.encode(modify({'type': type, 'buttons': button.value}))); + await _sendMouseUnchecked(type, button); } void enterOrLeave(bool enter) { @@ -982,6 +1066,13 @@ class InputModel { _pointerInsideImage = enter; _lastWheelTsUs = 0; + // Track active model for side button events (Linux). + if (enter) { + _activeSideButtonModel = this; + } else if (_activeSideButtonModel == this) { + _activeSideButtonModel = null; + } + // Fix status if (!enter) { resetModifiers(); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 4533f11fa..e94834a2b 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -3932,6 +3932,7 @@ class FFI { inputModel.resetModifiers(); // Dispose relative mouse mode resources to ensure cursor is restored inputModel.disposeRelativeMouseMode(); + inputModel.disposeSideButtonTracking(); if (closeSession) { await bind.sessionClose(sessionId: sessionId); } diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc index a05bb7856..210adba96 100644 --- a/flutter/linux/my_application.cc +++ b/flutter/linux/my_application.cc @@ -29,6 +29,80 @@ void try_set_transparent(GtkWindow* window, GdkScreen* screen, FlView* view); extern bool gIsConnectionManager; +// --- Side mouse button support (back/forward) --- +// Flutter's Linux embedder doesn't deliver X11 button 8/9 events to Dart. +// We intercept them via GDK and forward through a dedicated platform channel. + +static const char* kSideButtonChannelName = "org.rustdesk.rustdesk/side_buttons"; + +static gboolean on_side_button_event(GtkWidget* widget, GdkEventButton* event, gpointer user_data) { + if (event->button != 8 && event->button != 9) { + return FALSE; + } + // Ignore GDK_2BUTTON_PRESS / GDK_3BUTTON_PRESS (double/triple-click synthetic + // events) - only handle real press and release. + if (event->type != GDK_BUTTON_PRESS && event->type != GDK_BUTTON_RELEASE) { + return FALSE; + } + + FlMethodChannel* channel = FL_METHOD_CHANNEL(user_data); + if (channel == NULL) return FALSE; + + g_autoptr(FlValue) args = fl_value_new_map(); + fl_value_set_string_take(args, "button", + fl_value_new_string(event->button == 8 ? "back" : "forward")); + fl_value_set_string_take(args, "type", + fl_value_new_string(event->type == GDK_BUTTON_PRESS ? "down" : "up")); + + fl_method_channel_invoke_method(channel, "onSideMouseButton", args, + NULL, NULL, NULL); + + return TRUE; +} + +static FlMethodChannel* side_buttons_create_channel(FlEngine* engine) { + g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new(); + return fl_method_channel_new( + fl_engine_get_binary_messenger(engine), + kSideButtonChannelName, + FL_METHOD_CODEC(codec)); +} + +static void side_buttons_channel_destroy(gpointer data) { + g_object_unref(data); +} + +static void side_buttons_init_for_window(GtkWindow* window, FlMethodChannel* channel) { + // Guard against double-initialization (would leave dangling signal user_data). + if (g_object_get_data(G_OBJECT(window), "side-buttons-channel") != NULL) return; + + gtk_widget_add_events(GTK_WIDGET(window), + GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK); + // Store channel on the window so it stays alive and is freed with the window. + g_object_set_data_full(G_OBJECT(window), "side-buttons-channel", + g_object_ref(channel), side_buttons_channel_destroy); + g_signal_connect(window, "button-press-event", + G_CALLBACK(on_side_button_event), channel); + g_signal_connect(window, "button-release-event", + G_CALLBACK(on_side_button_event), channel); +} + +static void on_subwindow_created(FlPluginRegistry* registry) { +#if defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT) + wayland_shortcuts_inhibit_init_for_subwindow(registry); +#endif + // Set up side button forwarding for sub-windows. + if (registry == NULL || !FL_IS_VIEW(registry)) return; + FlView* view = FL_VIEW(registry); + GtkWidget* toplevel = gtk_widget_get_toplevel(GTK_WIDGET(view)); + if (toplevel != NULL && GTK_IS_WINDOW(toplevel)) { + FlMethodChannel* channel = side_buttons_create_channel(fl_view_get_engine(view)); + if (channel == NULL) return; + side_buttons_init_for_window(GTK_WINDOW(toplevel), channel); + g_object_unref(channel); // window now owns a ref via g_object_set_data_full + } +} + GtkWidget *find_gl_area(GtkWidget *widget); // Implements GApplication::activate. @@ -96,12 +170,12 @@ static void my_application_activate(GApplication* application) { gtk_widget_show(GTK_WIDGET(window)); gtk_widget_show(GTK_WIDGET(view)); -#if defined(GDK_WINDOWING_WAYLAND) && defined(HAS_KEYBOARD_SHORTCUTS_INHIBIT) - // Register callback for sub-windows created by desktop_multi_window plugin - // Only sub-windows (remote windows) need keyboard shortcuts inhibition + // Register callback for sub-windows created by desktop_multi_window plugin. + // Handles both Wayland shortcuts inhibition (guarded inside) and side button + // forwarding. Safe to call on X11-only builds - the plugin just stores the + // callback pointer regardless of windowing system. desktop_multi_window_plugin_set_window_created_callback( - (WindowCreatedCallback)wayland_shortcuts_inhibit_init_for_subwindow); -#endif + (WindowCreatedCallback)on_subwindow_created); fl_register_plugins(FL_PLUGIN_REGISTRY(view)); @@ -116,6 +190,11 @@ static void my_application_activate(GApplication* application) { self, nullptr); + // Forward side mouse button events (back/forward) to Dart on the main window. + FlMethodChannel* side_channel = side_buttons_create_channel(fl_view_get_engine(view)); + side_buttons_init_for_window(window, side_channel); + g_object_unref(side_channel); + gtk_widget_grab_focus(GTK_WIDGET(view)); } diff --git a/libs/enigo/src/linux/xdo.rs b/libs/enigo/src/linux/xdo.rs index 26d090855..7796904f9 100644 --- a/libs/enigo/src/linux/xdo.rs +++ b/libs/enigo/src/linux/xdo.rs @@ -8,6 +8,7 @@ use crate::{Key, KeyboardControllable, MouseButton, MouseControllable}; use hbb_common::libc::c_int; +use hbb_common::x11::xlib::{Display, XCloseDisplay, XGetPointerMapping, XOpenDisplay}; use libxdo_sys::{self, xdo_t, CURRENTWINDOW}; use std::{borrow::Cow, ffi::CString}; @@ -32,6 +33,51 @@ fn mousebutton(button: MouseButton) -> c_int { } } +/// Minimum number of buttons the X11 core pointer must support. +/// Buttons 8 (Back) and 9 (Forward) are needed for mouse side buttons. +const MIN_POINTER_BUTTONS: usize = 9; + +/// Check that the X11 core pointer's button map includes at least 9 buttons +/// so that `XTestFakeButtonEvent` can simulate Back (8) and Forward (9). +/// +/// RustDesk's uinput "Mouse passthrough" device normally provides enough +/// buttons, but we log a warning if the map is too small so the issue is +/// diagnosable. `XSetPointerMapping` cannot extend the button count (its +/// length must match `XGetPointerMapping`), so we only diagnose here. +fn check_x11_button_map() { + // Skip on non-X11 sessions to avoid noisy "XOpenDisplay failed" warnings + // on pure Wayland or headless environments without $DISPLAY. + if std::env::var_os("DISPLAY").is_none() { + return; + } + + let display: *mut Display = unsafe { XOpenDisplay(std::ptr::null()) }; + if display.is_null() { + log::warn!("XOpenDisplay failed, cannot check button map"); + return; + } + + let mut current_map = [0u8; 32]; + let nbuttons = + unsafe { XGetPointerMapping(display, current_map.as_mut_ptr(), current_map.len() as i32) }; + unsafe { XCloseDisplay(display) }; + + if nbuttons < 0 { + log::warn!("XGetPointerMapping failed (returned {nbuttons})"); + return; + } + + let nbuttons = nbuttons as usize; + if nbuttons >= MIN_POINTER_BUTTONS { + log::info!("X11 pointer has {nbuttons} buttons, side buttons supported"); + } else { + log::warn!( + "X11 pointer has only {nbuttons} buttons (need {MIN_POINTER_BUTTONS}); \ + back/forward side buttons may not work until a device with more buttons is added" + ); + } +} + /// The main struct for handling the event emitting pub(super) struct EnigoXdo { xdo: *mut xdo_t, @@ -52,6 +98,7 @@ impl Default for EnigoXdo { log::warn!("Failed to create xdo context, xdo functions will be disabled"); } else { log::info!("xdo context created successfully"); + check_x11_button_map(); } Self { xdo, From 3a1622e8b5664e5d76d0ca8b74646cce30c15e48 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 26 Apr 2026 21:25:31 +0800 Subject: [PATCH 233/277] refact(AGENTS.md): code rules, tokio (#14911) * refact(AGENTS.md): code rules, tokio Signed-off-by: fufesou * Update AGENTS.md * Update AGENTS.md * Update AGENTS.md * Update AGENTS.md --------- Signed-off-by: fufesou Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> --- AGENTS.md | 122 +++++++++++++++++------------------------------------- 1 file changed, 39 insertions(+), 83 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 68526d66d..e36c65fab 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,47 +1,18 @@ -# RustDesk Guide +# RustDesk Guide -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Development Commands - -### Build Commands -- `cargo run` - Build and run the desktop application (requires libsciter library) -- `python3 build.py --flutter` - Build Flutter version (desktop) -- `python3 build.py --flutter --release` - Build Flutter version in release mode -- `python3 build.py --hwcodec` - Build with hardware codec support -- `python3 build.py --vram` - Build with VRAM feature (Windows only) -- `cargo build --release` - Build Rust binary in release mode -- `cargo build --features hwcodec` - Build with specific features - -### Flutter Mobile Commands -- `cd flutter && flutter build android` - Build Android APK -- `cd flutter && flutter build ios` - Build iOS app -- `cd flutter && flutter run` - Run Flutter app in development mode -- `cd flutter && flutter test` - Run Flutter tests - -### Testing -- `cargo test` - Run Rust tests -- `cd flutter && flutter test` - Run Flutter tests - -### Platform-Specific Build Scripts -- `flutter/build_android.sh` - Android build script -- `flutter/build_ios.sh` - iOS build script -- `flutter/build_fdroid.sh` - F-Droid build script - -## Project Architecture +## Project Layout ### Directory Structure -- **`src/`** - Main Rust application code - - `src/ui/` - Legacy Sciter UI (deprecated, use Flutter instead) - - `src/server/` - Audio/clipboard/input/video services and network connections - - `src/client.rs` - Peer connection handling - - `src/platform/` - Platform-specific code -- **`flutter/`** - Flutter UI code for desktop and mobile -- **`libs/`** - Core libraries - - `libs/hbb_common/` - Video codec, config, network wrapper, protobuf, file transfer utilities - - `libs/scrap/` - Screen capture functionality - - `libs/enigo/` - Platform-specific keyboard/mouse control - - `libs/clipboard/` - Cross-platform clipboard implementation +* `src/` Rust app +* `src/server/` audio / clipboard / input / video / network +* `src/platform/` platform-specific code +* `src/ui/` legacy Sciter UI (deprecated) +* `flutter/` current UI +* `libs/hbb_common/` config / proto / shared utils +* `libs/scrap/` screen capture +* `libs/enigo/` input control +* `libs/clipboard/` clipboard +* `libs/hbb_common/src/config.rs` all options ### Key Components - **Remote Desktop Protocol**: Custom protocol implemented in `src/rendezvous_mediator.rs` for communicating with rustdesk-server @@ -57,50 +28,35 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - Mobile: `flutter/lib/mobile/` - Shared: `flutter/lib/common/` and `flutter/lib/models/` -## Important Build Notes - -### Dependencies -- Requires vcpkg for C++ dependencies: `libvpx`, `libyuv`, `opus`, `aom` -- Set `VCPKG_ROOT` environment variable -- Download appropriate Sciter library for legacy UI support - -### Ignore Patterns -When working with files, ignore these directories: -- `target/` - Rust build artifacts -- `flutter/build/` - Flutter build output -- `flutter/.dart_tool/` - Flutter tooling files - -### Cross-Platform Considerations -- Windows builds require additional DLLs and virtual display drivers -- macOS builds need proper signing and notarization for distribution -- Linux builds support multiple package formats (deb, rpm, AppImage) -- Mobile builds require platform-specific toolchains (Android SDK, Xcode) - -### Feature Flags -- `hwcodec` - Hardware video encoding/decoding -- `vram` - VRAM optimization (Windows only) -- `flutter` - Enable Flutter UI -- `unix-file-copy-paste` - Unix file clipboard support -- `screencapturekit` - macOS ScreenCaptureKit (macOS only) - -### Config -All configurations or options are under `libs/hbb_common/src/config.rs` file, 4 types: -- Settings -- Local -- Display -- Built-in - ## Rust Rules -- In Rust code, do not introduce `unwrap()` or `expect()`. -- Allowed exceptions: -- Tests may use `unwrap()` or `expect()` when it keeps the test focused and readable. -- Lock acquisition may use `unwrap()` only when the locking API makes that the practical option and the failure mode is poison handling rather than normal control flow. -- Outside those exceptions, propagate errors, handle them explicitly, or use safer fallbacks instead of `unwrap()` and `expect()`. +* Avoid `unwrap()` / `expect()` in production code. +* Exceptions: + + * tests; + * lock acquisition where failure means poisoning, not normal control flow. +* Otherwise prefer `Result` + `?` or explicit handling. +* Do not ignore errors silently. +* Avoid unnecessary `.clone()`. +* Prefer borrowing when practical. +* Do not add dependencies unless needed. +* Keep code simple and idiomatic. + +## Tokio Rules + +* Assume a Tokio runtime already exists. +* Never create nested runtimes. +* Never call `Runtime::block_on()` inside Tokio / async code. +* Do not hide runtime creation inside helpers or libraries. +* Do not hold locks across `.await`. +* Prefer `.await`, `tokio::spawn`, channels. +* Use `spawn_blocking` or dedicated threads for blocking work. +* Do not use `std::thread::sleep()` in async code. ## Editing Hygiene -- Do not introduce formatting-only changes. -- Do not run repository-wide formatters or reflow unrelated code unless the - user explicitly asks for formatting. -- Keep diffs limited to semantic changes required for the task. +* Change only what is required. +* Prefer the smallest valid diff. +* Do not refactor unrelated code. +* Do not make formatting-only changes. +* Keep naming/style consistent with nearby code. From 5ea6714db8c47e4eb660c22cd55208ce3dce828d Mon Sep 17 00:00:00 2001 From: Azhar Date: Sun, 26 Apr 2026 18:58:05 +0530 Subject: [PATCH 234/277] Fix: replace unwrap() with proper error handling in CLI password prompt (#14910) Signed-off-by: bunnysayzz --- src/cli.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/cli.rs b/src/cli.rs index f61bfe92f..2f3b3550f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -25,7 +25,13 @@ impl Session { pub fn new(id: &str, sender: mpsc::UnboundedSender) -> Self { let mut password = "".to_owned(); if PeerConfig::load(id).password.is_empty() { - password = rpassword::prompt_password("Enter password: ").unwrap(); + match rpassword::prompt_password("Enter password: ") { + Ok(p) => password = p, + Err(e) => { + log::error!("Failed to read password: {:?}", e); + password = "".to_owned(); + } + } } let session = Self { id: id.to_owned(), From c8ba99d1a1c5c293e7b5ab9b3abc1bb5f3cc0cb9 Mon Sep 17 00:00:00 2001 From: Amirhosein Akhlaghpoor Date: Sun, 26 Apr 2026 14:44:26 +0000 Subject: [PATCH 235/277] flutter: shift after one shot IME capitalization (#14695) * flutter: shift after one shot IME capitalization Signed-off-by: Amirhossein Akhlaghpour * flutter: clarify stale mobile shift handling Signed-off-by: Amirhossein Akhlaghpour * fix(android): gboard shift stuck Signed-off-by: fufesou * fix(android): gboard shift stuck, remove unused param Signed-off-by: fufesou * fix(android): gboard shift stuck, release shift before sending events Signed-off-by: fufesou * chore(flutter): document stale mobile shift release flow Signed-off-by: Amirhossein Akhlaghpour --------- Signed-off-by: Amirhossein Akhlaghpour Signed-off-by: fufesou Co-authored-by: fufesou --- flutter/lib/models/input_model.dart | 72 +++++++++++ flutter/lib/models/input_modifier_utils.dart | 38 ++++++ flutter/pubspec.yaml | 4 +- flutter/test/input_modifier_utils_test.dart | 125 +++++++++++++++++++ 4 files changed, 237 insertions(+), 2 deletions(-) create mode 100644 flutter/lib/models/input_modifier_utils.dart create mode 100644 flutter/test/input_modifier_utils_test.dart diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index ab9278217..427072677 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; +import 'package:flutter/foundation.dart'; import 'dart:ui' as ui; import 'package:desktop_multi_window/desktop_multi_window.dart'; @@ -15,6 +16,7 @@ import 'package:get/get.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; import '../../models/state_model.dart'; +import 'input_modifier_utils.dart'; import 'relative_mouse_model.dart'; import '../common.dart'; import '../consts.dart'; @@ -697,6 +699,38 @@ class InputModel { } } + // Safe: this only re-dispatches synthesized Shift key-up events. + // The key-up path clears the tracked Shift state so this does not loop. + void _releaseTrackedShiftKeyEventIfNeeded() { + final leftShift = toReleaseKeys.lastLShiftKeyEvent; + final rightShift = toReleaseKeys.lastRShiftKeyEvent; + if (leftShift != null) { + handleKeyEvent(leftShift); + } + if (rightShift != null) { + handleKeyEvent(rightShift); + } + } + + // Safe: this only re-dispatches synthesized Shift key-up events. + // The raw key-up path clears the tracked Shift state so this does not loop. + void _releaseTrackedRawShiftKeyEventIfNeeded() { + final leftShift = toReleaseRawKeys.lastLShiftKeyEvent; + final rightShift = toReleaseRawKeys.lastRShiftKeyEvent; + if (leftShift != null) { + handleRawKeyEvent(RawKeyUpEvent( + data: leftShift.data, + character: leftShift.character, + )); + } + if (rightShift != null) { + handleRawKeyEvent(RawKeyUpEvent( + data: rightShift.data, + character: rightShift.character, + )); + } + } + KeyEventResult handleRawKeyEvent(RawKeyEvent e) { if (isViewOnly) return KeyEventResult.handled; if (isViewCamera) return KeyEventResult.handled; @@ -751,6 +785,27 @@ class InputModel { toReleaseRawKeys.updateKeyUp(key, e); } + // On some mobile soft-keyboard paths, Flutter may leave cached Shift state + // set even though the current raw key event is not shifted anymore. + if (e is RawKeyDownEvent && + shouldReleaseStaleMobileShift( + isMobile: isMobile, + cachedShiftPressed: shift, + actualShiftPressed: e.isShiftPressed, + logicalKey: e.logicalKey, + hasTrackedShiftKeyDown: toReleaseRawKeys.lastLShiftKeyEvent != null || + toReleaseRawKeys.lastRShiftKeyEvent != null, + )) { + if (kDebugMode) { + debugPrint( + 'input: releasing stale mobile Shift before replaying tracked raw ' + 'key-up (logicalKey=${e.logicalKey.keyLabel}, ' + 'actualShiftPressed=${e.isShiftPressed}, cachedShiftPressed=$shift)', + ); + } + _releaseTrackedRawShiftKeyEventIfNeeded(); + } + // * Currently mobile does not enable map mode if ((isDesktop || isWebDesktop) && keyboardMode == kKeyMapMode) { mapKeyboardModeRaw(e, iosCapsLock); @@ -794,6 +849,8 @@ class InputModel { iosCapsLock = _getIosCapsFromCharacter(e); } + // Update cached modifier state before sending the event. The stale mobile + // Shift release check below relies on this cached state. if (e is KeyUpEvent) { handleKeyUpEventModifiers(e); } else if (e is KeyDownEvent) { @@ -831,6 +888,21 @@ class InputModel { } } } + + // On some mobile soft-keyboard paths, Flutter may leave cached Shift state + // set even though the current key event is not shifted anymore. + if (e is KeyDownEvent && + shouldReleaseStaleMobileShift( + isMobile: isMobile, + cachedShiftPressed: shift, + actualShiftPressed: HardwareKeyboard.instance.isShiftPressed, + logicalKey: e.logicalKey, + hasTrackedShiftKeyDown: toReleaseKeys.lastLShiftKeyEvent != null || + toReleaseKeys.lastRShiftKeyEvent != null, + )) { + _releaseTrackedShiftKeyEventIfNeeded(); + } + final isDesktopAndMapMode = isDesktop || (isWebDesktop && keyboardMode == kKeyMapMode); if (isMobileAndMapMode || isDesktopAndMapMode) { diff --git a/flutter/lib/models/input_modifier_utils.dart b/flutter/lib/models/input_modifier_utils.dart new file mode 100644 index 000000000..e65c32790 --- /dev/null +++ b/flutter/lib/models/input_modifier_utils.dart @@ -0,0 +1,38 @@ +import 'package:flutter/services.dart'; + +/// Returns true when a stale mobile one-shot Shift state should be released +/// by replaying a tracked Shift key-down as a synthesized key-up. +/// +/// This is only valid on mobile when Flutter's cached Shift state is still on +/// (`cachedShiftPressed == true`) but the current hardware/raw event reports +/// Shift as off (`actualShiftPressed == false`). +/// +/// A tracked Shift key-down is required so the caller can safely synthesize the +/// matching key-up. Both `shiftLeft` and `shiftRight` are excluded because the +/// Shift key event itself must be processed first; otherwise we could release +/// the tracked key while still handling the original Shift press/release. +/// Callers should evaluate this only after their cached modifier state has been +/// updated for the current event. +/// +/// When this returns true, the caller logs a line like: +/// `input: releasing stale mobile Shift before replaying tracked raw key-up` +/// immediately before calling `_releaseTrackedRawShiftKeyEventIfNeeded()`. +bool shouldReleaseStaleMobileShift({ + required bool isMobile, + required bool cachedShiftPressed, + required bool actualShiftPressed, + required LogicalKeyboardKey logicalKey, + required bool hasTrackedShiftKeyDown, +}) { + if (!isMobile || !cachedShiftPressed || actualShiftPressed) { + return false; + } + if (!hasTrackedShiftKeyDown) { + return false; + } + if (logicalKey == LogicalKeyboardKey.shiftLeft || + logicalKey == LogicalKeyboardKey.shiftRight) { + return false; + } + return true; +} diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index eb6d76161..eddf5a19d 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -113,8 +113,8 @@ dependencies: dev_dependencies: icons_launcher: ^2.0.4 - #flutter_test: - #sdk: flutter + flutter_test: + sdk: flutter build_runner: ^2.4.6 freezed: ^2.4.2 flutter_lints: ^2.0.2 diff --git a/flutter/test/input_modifier_utils_test.dart b/flutter/test/input_modifier_utils_test.dart new file mode 100644 index 000000000..2e1971753 --- /dev/null +++ b/flutter/test/input_modifier_utils_test.dart @@ -0,0 +1,125 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_hbb/models/input_modifier_utils.dart'; + +void main() { + group('shouldReleaseStaleMobileShift', () { + test('does not release when cached shift is already false', () { + expect( + shouldReleaseStaleMobileShift( + isMobile: true, + cachedShiftPressed: false, + actualShiftPressed: false, + logicalKey: LogicalKeyboardKey.keyD, + hasTrackedShiftKeyDown: true, + ), + isFalse, + ); + }); + + test('releases one-shot mobile shift after a text key', () { + expect( + shouldReleaseStaleMobileShift( + isMobile: true, + cachedShiftPressed: true, + actualShiftPressed: false, + logicalKey: LogicalKeyboardKey.keyD, + hasTrackedShiftKeyDown: true, + ), + isTrue, + ); + }); + + test('does not release manually toggled shift without tracked key down', + () { + expect( + shouldReleaseStaleMobileShift( + isMobile: true, + cachedShiftPressed: true, + actualShiftPressed: false, + logicalKey: LogicalKeyboardKey.keyD, + hasTrackedShiftKeyDown: false, + ), + isFalse, + ); + }); + + test('does not release when shift is still physically pressed', () { + expect( + shouldReleaseStaleMobileShift( + isMobile: true, + cachedShiftPressed: true, + actualShiftPressed: true, + logicalKey: LogicalKeyboardKey.keyD, + hasTrackedShiftKeyDown: true, + ), + isFalse, + ); + }); + + test('does not release on non-mobile platforms', () { + expect( + shouldReleaseStaleMobileShift( + isMobile: false, + cachedShiftPressed: true, + actualShiftPressed: false, + logicalKey: LogicalKeyboardKey.keyD, + hasTrackedShiftKeyDown: true, + ), + isFalse, + ); + }); + + test('releases on enter key', () { + expect( + shouldReleaseStaleMobileShift( + isMobile: true, + cachedShiftPressed: true, + actualShiftPressed: false, + logicalKey: LogicalKeyboardKey.enter, + hasTrackedShiftKeyDown: true, + ), + isTrue, + ); + }); + + test('releases on arrow key', () { + expect( + shouldReleaseStaleMobileShift( + isMobile: true, + cachedShiftPressed: true, + actualShiftPressed: false, + logicalKey: LogicalKeyboardKey.arrowLeft, + hasTrackedShiftKeyDown: true, + ), + isTrue, + ); + }); + + test('does not release on modifier events', () { + expect( + shouldReleaseStaleMobileShift( + isMobile: true, + cachedShiftPressed: true, + actualShiftPressed: false, + logicalKey: LogicalKeyboardKey.shiftLeft, + hasTrackedShiftKeyDown: true, + ), + isFalse, + ); + }); + + test('does not release on shiftRight modifier events', () { + expect( + shouldReleaseStaleMobileShift( + isMobile: true, + cachedShiftPressed: true, + actualShiftPressed: false, + logicalKey: LogicalKeyboardKey.shiftRight, + hasTrackedShiftKeyDown: true, + ), + isFalse, + ); + }); + }); +} From 7308c448f177c9a22d2c9300227425406ebd7fb7 Mon Sep 17 00:00:00 2001 From: Sergiusz Michalik Date: Sun, 26 Apr 2026 16:46:41 +0200 Subject: [PATCH 236/277] fix(client): serialize X11 keyboard grab and debounce focus feedback (#14836) * fix(client): serialize X11 keyboard grab and debounce focus feedback When two RustDesk sessions run fullscreen on separate monitors on Linux/X11, keyboard input gets stuck on the wrong session or stops working entirely. This happens because each Flutter isolate calls change_grab_status concurrently, racing on KEYBOARD_HOOKED and the rdev grab channel. Additionally, XGrabKeyboard causes a focus-change feedback loop: grab shifts focus away from the Flutter window, triggering PointerExit, which releases the grab, restoring focus, triggering PointerEnter, which re-grabs -- cycling at ~10 Hz and blocking keyboard input. Fix by: - Serializing grab transitions with a mutex and tracking the owning session (by lc.session_id), so a stale Wait from session A cannot clobber session B's freshly acquired grab. - Debouncing Wait events (300 ms) from the same session that just acquired the grab, breaking the X11 focus feedback loop. - Refreshing the debounce timer on idempotent Run calls (enterView while already owner), keeping the grab stable during normal use. Signed-off-by: Sergiusz Michalik * fix(client): add deferred release and dedup for debounced Wait When a Wait is debounced (within 300ms of grab acquisition), schedule a deferred release thread that re-checks after the debounce window. If no new Run refreshed the grab, the deferred thread releases it, ensuring a genuine leave within the debounce window is not lost. Add a deferred_pending flag to GrabOwnerState to prevent spawning redundant threads during the X11 focus feedback loop. Signed-off-by: Sergiusz Michalik * fix(client): use window-scoped ID and fix deferred-release re-arming Address PR review feedback: - Use per-window UUID instead of connection-scoped lc.session_id so two windows viewing the same peer get distinct grab owners - Reset deferred_pending on both idempotent Run refresh and owner handoff, so a subsequent Wait can always spawn a fresh timer - Replace manual Default impl with derive * fix(client): recover from poisoned mutex instead of panicking * docs: clarify cross-platform rationale for GrabOwnerState * fix(client): only clear deferred_pending when timer snapshot matches * fix(client): use full u128 window ID, downgrade grab logs to debug - Widen GrabOwnerState.owner to u128 to avoid theoretical collision from truncating a 128-bit UUID to 64 bits - Downgrade all grab transition log::info! to log::debug! to reduce log noise during routine window switches - Clear deferred_pending on post-debounce release path to maintain the "deferred_pending => timer in flight" invariant * fix(client): gate GRAB_DEBOUNCE_MS with cfg(target_os = "linux") * fix(grab): release grabbed keys without clobbering new owner state Signed-off-by: fufesou * fix(keyboard): Simple refactor Signed-off-by: fufesou --------- Signed-off-by: Sergiusz Michalik Signed-off-by: fufesou Co-authored-by: fufesou --- src/flutter_ffi.rs | 21 +++- src/keyboard.rs | 223 +++++++++++++++++++++++++++++++++--- src/ui_session_interface.rs | 6 +- 3 files changed, 229 insertions(+), 21 deletions(-) diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 2d339f5c2..1ee13f4df 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -605,21 +605,30 @@ pub fn session_handle_flutter_raw_key_event( } } -// SyncReturn<()> is used to make sure enter() and leave() are executed in the sequence this function is called. -// // If the cursor jumps between remote page of two connections, leave view and enter view will be called. // session_enter_or_leave() will be called then. -// As rust is multi-thread, it is possible that enter() is called before leave(). -// This will cause the keyboard input to take no effect. +// As Rust is multi-threaded, enter() can be called before leave(). +// The Rust-side grab ownership state filters stale transitions. pub fn session_enter_or_leave(_session_id: SessionID, _enter: bool) -> SyncReturn<()> { #[cfg(not(any(target_os = "android", target_os = "ios")))] if let Some(session) = sessions::get_session_by_session_id(&_session_id) { let keyboard_mode = session.get_keyboard_mode(); + // Use the full per-window UUID (not lc.session_id which is per-connection) + // so that two windows viewing the same peer get distinct grab owners. + let window_id = _session_id.as_u128(); if _enter { set_cur_session_id_(_session_id, &keyboard_mode); - session.enter(keyboard_mode); + crate::keyboard::client::change_grab_status( + crate::common::GrabState::Run, + &keyboard_mode, + window_id, + ); } else { - session.leave(keyboard_mode); + crate::keyboard::client::change_grab_status( + crate::common::GrabState::Wait, + &keyboard_mode, + window_id, + ); } } SyncReturn(()) diff --git a/src/keyboard.rs b/src/keyboard.rs index c5d4dfde8..b9cf4da2d 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -82,8 +82,67 @@ lazy_static::lazy_static! { pub mod client { use super::*; + /// Tracks grab ownership and serializes transitions across threads. + /// + /// Multiple Flutter isolates (one per session window) call + /// `change_grab_status(Run/Wait)` concurrently. Without serialization a + /// stale `Wait` from session A can clobber session B's freshly acquired + /// grab on any desktop OS. + /// + /// Windows and macOS are less susceptible in practice because the Flutter + /// side triggers `enterView` only after a mouse click inside the window, + /// but we cannot rely on that. On Linux/X11, `XGrabKeyboard` can also + /// cause a focus-change feedback loop (~10 Hz), so `last_grab` debounces + /// spurious `Wait` events that arrive shortly after a `Run`. + #[derive(Default)] + struct GrabOwnerState { + owner: Option, + last_grab: Option, + /// True while a deferred-release thread is in flight. Prevents + /// spawning redundant threads during the X11 feedback loop. + deferred_pending: bool, + } + + /// How long after a grab acquisition we suppress Wait from the same session. + /// Must exceed one full X11 feedback cycle (~100 ms: 50 ms enable + 50 ms disable). + #[cfg(target_os = "linux")] + const GRAB_DEBOUNCE_MS: u128 = 300; + lazy_static::lazy_static! { static ref IS_GRAB_STARTED: Arc> = Arc::new(Mutex::new(false)); + static ref GRAB_STATE: Arc> = Arc::new(Mutex::new(GrabOwnerState::default())); + } + + #[cfg(target_os = "linux")] + lazy_static::lazy_static! { + static ref GRAB_OP_LOCK: Mutex<()> = Mutex::new(()); + } + + #[cfg(target_os = "linux")] + fn apply_run_grab_if_owner(session_id: u128, disable_first: bool) { + let _lock = GRAB_OP_LOCK.lock().unwrap(); + let gs = GRAB_STATE.lock().unwrap(); + if gs.owner != Some(session_id) { + return; + } + drop(gs); + if disable_first { + log::debug!("[grab] handoff: disable_grab before re-grab"); + rdev::disable_grab(); + } + rdev::enable_grab(); + } + + #[cfg(target_os = "linux")] + fn disable_grab_if_released() { + let _lock = GRAB_OP_LOCK.lock().unwrap(); + let should_disable = { + let gs = GRAB_STATE.lock().unwrap(); + gs.owner.is_none() && gs.last_grab.is_none() + }; + if should_disable { + rdev::disable_grab(); + } } pub fn start_grab_loop() { @@ -96,36 +155,167 @@ pub mod client { } #[cfg(not(any(target_os = "android", target_os = "ios")))] - pub fn change_grab_status(state: GrabState, keyboard_mode: &str) { + pub fn change_grab_status(state: GrabState, keyboard_mode: &str, session_id: u128) { #[cfg(feature = "flutter")] if !IS_RDEV_ENABLED.load(Ordering::SeqCst) { return; } + // Serialize transitions so a stale `Wait` from a previous owner cannot + // clobber a fresh `Run` from a different session window. + let mut release_after_unlock = None; + #[cfg(target_os = "linux")] + let mut run_grab_after_unlock = None; + #[cfg(target_os = "linux")] + let mut disable_after_unlock = false; + let mut gs = GRAB_STATE.lock().unwrap(); match state { GrabState::Ready => {} GrabState::Run => { #[cfg(windows)] update_grab_get_key_name(keyboard_mode); + + // Idempotent: if this session already owns the grab, just + // refresh the debounce timer (proves the session is still + // actively focused) and skip the actual grab call. + if gs.owner == Some(session_id) { + gs.last_grab = Some(std::time::Instant::now()); + // Reset so the next Wait can spawn a fresh deferred-release + // timer with an up-to-date snapshot of last_grab. + gs.deferred_pending = false; + log::debug!( + "[grab] Run(0x{:x}): already owner, refresh debounce", + session_id + ); + return; + } + + log::debug!( + "[grab] Run(0x{:x}): prev_owner={}, mode={}", + session_id, + gs.owner + .map_or("none".to_string(), |id| format!("0x{:x}", id)), + keyboard_mode, + ); + #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] - KEYBOARD_HOOKED.swap(true, Ordering::SeqCst); + KEYBOARD_HOOKED.store(true, Ordering::SeqCst); #[cfg(target_os = "linux")] - rdev::enable_grab(); + let had_owner = gs.owner.is_some(); + gs.owner = Some(session_id); + gs.last_grab = Some(std::time::Instant::now()); + // Invalidate any in-flight deferred release from the previous + // owner so it cannot suppress a fresh timer for the new owner. + gs.deferred_pending = false; + #[cfg(target_os = "linux")] + { + run_grab_after_unlock = Some(had_owner); + } } GrabState::Wait => { + // Drop stale `Wait` events that do not correspond to the + // current grab owner. This prevents a late PointerExit from + // session A from releasing session B's freshly acquired grab. + if gs.owner != Some(session_id) { + log::debug!( + "[grab] Wait(0x{:x}): ignored, owner={}", + session_id, + gs.owner + .map_or("none".to_string(), |id| format!("0x{:x}", id)), + ); + return; + } + + // Debounce: on Linux/X11, XGrabKeyboard causes a focus-change + // feedback loop (grab -> PointerExit -> ungrab -> PointerEnter -> + // grab -> ...). Suppress Wait if the grab was acquired recently + // by this same session -- it is X11 feedback, not a real leave. + // A deferred release is scheduled so that a genuine leave within + // the debounce window is not permanently lost. + #[cfg(target_os = "linux")] + if let Some(t) = gs.last_grab { + let elapsed = t.elapsed().as_millis(); + if elapsed < GRAB_DEBOUNCE_MS { + if !gs.deferred_pending { + log::debug!( + "[grab] Wait(0x{:x}): debounced ({}ms < {}ms), scheduling deferred release", + session_id, elapsed, GRAB_DEBOUNCE_MS, + ); + gs.deferred_pending = true; + let remaining = (GRAB_DEBOUNCE_MS - elapsed) as u64 + 50; + let snapshot = gs.last_grab; + let mode = keyboard_mode.to_string(); + std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_millis(remaining)); + let release_keys = { + let mut gs = GRAB_STATE.lock().unwrap(); + // Release only if no new Run has refreshed the grab since. + if gs.owner == Some(session_id) && gs.last_grab == snapshot { + let to_release = take_remote_keys(); + gs.deferred_pending = false; + log::debug!( + "[grab] Wait(0x{:x}): deferred release", + session_id + ); + KEYBOARD_HOOKED.store(false, Ordering::SeqCst); + gs.owner = None; + gs.last_grab = None; + Some(to_release) + } else { + log::debug!( + "[grab] Wait(0x{:x}): deferred release cancelled (grab refreshed)", + session_id, + ); + None + } + }; + if let Some(to_release) = release_keys { + disable_grab_if_released(); + release_remote_keys_for_events(&mode, to_release); + } + }); + } else { + log::debug!( + "[grab] Wait(0x{:x}): debounced, deferred release already pending", + session_id, + ); + } + return; + } + } + + log::debug!("[grab] Wait(0x{:x}): releasing grab", session_id); + #[cfg(windows)] rdev::set_get_key_unicode(false); - release_remote_keys(keyboard_mode); - #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] - KEYBOARD_HOOKED.swap(false, Ordering::SeqCst); + KEYBOARD_HOOKED.store(false, Ordering::SeqCst); + gs.owner = None; + gs.last_grab = None; + gs.deferred_pending = false; + release_after_unlock = Some(take_remote_keys()); #[cfg(target_os = "linux")] - rdev::disable_grab(); + { + disable_after_unlock = true; + } } GrabState::Exit => {} } + drop(gs); + #[cfg(target_os = "linux")] + { + if disable_after_unlock { + disable_grab_if_released(); + } + if let Some(disable_first) = run_grab_after_unlock { + apply_run_grab_if_owner(session_id, disable_first); + } + } + if let Some(to_release) = release_after_unlock { + release_remote_keys_for_events(keyboard_mode, to_release); + } } pub fn process_event(keyboard_mode: &str, event: &Event, lock_modes: Option) { @@ -341,7 +531,6 @@ fn notify_exit_relative_mouse_mode() { flutter::push_session_event(&session_id, "exit_relative_mouse_mode", vec![]); } - /// Handle relative mouse mode shortcuts in the rdev grab loop. /// Returns true if the event should be blocked from being sent to the peer. #[cfg(feature = "flutter")] @@ -540,10 +729,12 @@ pub fn is_long_press(event: &Event) -> bool { return false; } -pub fn release_remote_keys(keyboard_mode: &str) { - // todo!: client quit suddenly, how to release keys? - let to_release = TO_RELEASE.lock().unwrap().clone(); - TO_RELEASE.lock().unwrap().clear(); +fn take_remote_keys() -> HashMap { + let mut to_release = TO_RELEASE.lock().unwrap(); + std::mem::take(&mut *to_release) +} + +fn release_remote_keys_for_events(keyboard_mode: &str, to_release: HashMap) { for (key, mut event) in to_release.into_iter() { event.event_type = EventType::KeyRelease(key); client::process_event(keyboard_mode, &event, None); @@ -558,6 +749,12 @@ pub fn release_remote_keys(keyboard_mode: &str) { } } +#[allow(dead_code)] +pub fn release_remote_keys(keyboard_mode: &str) { + // todo!: client quit suddenly, how to release keys? + release_remote_keys_for_events(keyboard_mode, take_remote_keys()); +} + pub fn get_keyboard_mode_enum(keyboard_mode: &str) -> KeyboardMode { match keyboard_mode { "map" => KeyboardMode::Map, @@ -748,7 +945,6 @@ pub fn event_to_key_events( ) -> Vec { peer.retain(|c| !c.is_whitespace()); - let mut key_event = KeyEvent::new(); update_modifiers_state(event); match event.event_type { @@ -761,6 +957,7 @@ pub fn event_to_key_events( _ => {} } + let mut key_event = KeyEvent::new(); key_event.mode = keyboard_mode.into(); let mut key_events = match keyboard_mode { diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index be1895e64..c18c17fe2 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -870,12 +870,14 @@ impl Session { #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn enter(&self, keyboard_mode: String) { - keyboard::client::change_grab_status(GrabState::Run, &keyboard_mode); + let session_id = self.lc.read().unwrap().session_id as u128; + keyboard::client::change_grab_status(GrabState::Run, &keyboard_mode, session_id); } #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn leave(&self, keyboard_mode: String) { - keyboard::client::change_grab_status(GrabState::Wait, &keyboard_mode); + let session_id = self.lc.read().unwrap().session_id as u128; + keyboard::client::change_grab_status(GrabState::Wait, &keyboard_mode, session_id); } // flutter only TODO new input From 5b7ad339b899a17aa3bc591bf16b20ee8a84ac9d Mon Sep 17 00:00:00 2001 From: s1korrrr Date: Mon, 27 Apr 2026 13:44:35 +0200 Subject: [PATCH 237/277] fix(iPad): keep touch gestures with external mouse (#14652) * fix(ipad): keep touch gestures with external mouse Signed-off-by: Rafal * fix(mobile): touch gesture on physical mouse connected Signed-off-by: fufesou * fix(ipad): revert 9ee100b53e7a3f336122f827c814b363f7a9f9dc keep touch gestures with external mouse Signed-off-by: fufesou * fix(mobile): align view camera page with remote page Signed-off-by: fufesou --------- Signed-off-by: Rafal Signed-off-by: fufesou Co-authored-by: fufesou --- flutter/lib/common/widgets/remote_input.dart | 24 ++++++++++++++----- flutter/lib/mobile/pages/remote_page.dart | 10 ++++---- .../lib/mobile/pages/view_camera_page.dart | 12 ++++------ 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index 5871033db..9515ca759 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -532,7 +532,9 @@ class _RawTouchGestureDetectorRegionState // Official TapGestureRecognizer: GestureRecognizerFactoryWithHandlers( - () => TapGestureRecognizer(), (instance) { + () => TapGestureRecognizer( + supportedDevices: kTouchBasedDeviceKinds, + ), (instance) { instance ..onTapDown = onTapDown ..onTapUp = onTapUp @@ -540,14 +542,18 @@ class _RawTouchGestureDetectorRegionState }), DoubleTapGestureRecognizer: GestureRecognizerFactoryWithHandlers( - () => DoubleTapGestureRecognizer(), (instance) { + () => DoubleTapGestureRecognizer( + supportedDevices: kTouchBasedDeviceKinds, + ), (instance) { instance ..onDoubleTapDown = onDoubleTapDown ..onDoubleTap = onDoubleTap; }), LongPressGestureRecognizer: GestureRecognizerFactoryWithHandlers( - () => LongPressGestureRecognizer(), (instance) { + () => LongPressGestureRecognizer( + supportedDevices: kTouchBasedDeviceKinds, + ), (instance) { instance ..onLongPressDown = onLongPressDown ..onLongPressUp = onLongPressUp @@ -557,7 +563,9 @@ class _RawTouchGestureDetectorRegionState // Customized HoldTapMoveGestureRecognizer: GestureRecognizerFactoryWithHandlers( - () => HoldTapMoveGestureRecognizer(), + () => HoldTapMoveGestureRecognizer( + supportedDevices: kTouchBasedDeviceKinds, + ), (instance) => instance ..onHoldDragStart = onHoldDragStart ..onHoldDragUpdate = onHoldDragUpdate @@ -565,14 +573,18 @@ class _RawTouchGestureDetectorRegionState ..onHoldDragEnd = onHoldDragEnd), DoubleFinerTapGestureRecognizer: GestureRecognizerFactoryWithHandlers( - () => DoubleFinerTapGestureRecognizer(), (instance) { + () => DoubleFinerTapGestureRecognizer( + supportedDevices: kTouchBasedDeviceKinds, + ), (instance) { instance ..onDoubleFinerTap = onDoubleFinerTap ..onDoubleFinerTapDown = onDoubleFinerTapDown; }), CustomTouchGestureRecognizer: GestureRecognizerFactoryWithHandlers( - () => CustomTouchGestureRecognizer(), (instance) { + () => CustomTouchGestureRecognizer( + supportedDevices: kTouchBasedDeviceKinds, + ), (instance) { instance.onOneFingerPanStart = (DragStartDetails d) => onOneFingerPanStart(context, d); instance diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 9102d163c..9064c122b 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -426,12 +426,10 @@ class _RemotePageState extends State with WidgetsBindingObserver { } return Container( color: MyTheme.canvasColor, - child: inputModel.isPhysicalMouse.value - ? getBodyForMobile() - : RawTouchGestureDetectorRegion( - child: getBodyForMobile(), - ffi: gFFI, - ), + child: RawTouchGestureDetectorRegion( + child: getBodyForMobile(), + ffi: gFFI, + ), ); }), ), diff --git a/flutter/lib/mobile/pages/view_camera_page.dart b/flutter/lib/mobile/pages/view_camera_page.dart index 0898125c4..08c8cda1a 100644 --- a/flutter/lib/mobile/pages/view_camera_page.dart +++ b/flutter/lib/mobile/pages/view_camera_page.dart @@ -259,13 +259,11 @@ class _ViewCameraPageState extends State } return Container( color: MyTheme.canvasColor, - child: inputModel.isPhysicalMouse.value - ? getBodyForMobile() - : RawTouchGestureDetectorRegion( - child: getBodyForMobile(), - ffi: gFFI, - isCamera: true, - ), + child: RawTouchGestureDetectorRegion( + child: getBodyForMobile(), + ffi: gFFI, + isCamera: true, + ), ); }), ), From 1e6a3dc6445c3b9c0abb5fcd6454dbc6d8e154a7 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 27 Apr 2026 22:37:22 +0800 Subject: [PATCH 238/277] fix(android): waiting for image, one cause (#14919) Signed-off-by: fufesou --- .../kotlin/com/carriez/flutter_hbb/AudioRecordHandle.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/AudioRecordHandle.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/AudioRecordHandle.kt index db222dc84..05742d7fd 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/AudioRecordHandle.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/AudioRecordHandle.kt @@ -62,7 +62,13 @@ class AudioRecordHandle(private var context: Context, private var isVideoStart: return false } } - audioRecorder = builder.build() + val recorder = try { + builder.build() + } catch (e: Exception) { + Log.e(logTag, "createAudioRecorder failed", e) + return false + } + audioRecorder = recorder Log.d(logTag, "createAudioRecorder done,minBufferSize:$minBufferSize") return true } From 99b565ef40408e1ddcb5432c206b87cddf8adef3 Mon Sep 17 00:00:00 2001 From: s1korrrr Date: Tue, 28 Apr 2026 04:55:28 +0200 Subject: [PATCH 239/277] fix(iOS): preserve local pasteboard sync from Windows hosts (#14659) * fix(ios): accept windows clipboard updates locally Signed-off-by: Rafal * docs: document clipboard text helpers * fix(iOS): sync clipboard, debug Signed-off-by: fufesou --------- Signed-off-by: Rafal Signed-off-by: fufesou Co-authored-by: fufesou --- src/client/io_loop.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 78d9a4e40..e8afa8e01 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1448,6 +1448,23 @@ impl Remote { if !self.handler.lc.read().unwrap().disable_clipboard.v { #[cfg(not(any(target_os = "android", target_os = "ios")))] update_clipboard(_mcb.clipboards, ClipboardSide::Client); + #[cfg(target_os = "ios")] + { + if let Some(cb) = _mcb + .clipboards + .iter() + .find(|c| c.format.enum_value() == Ok(ClipboardFormat::Text)) + { + let content = if cb.compress { + hbb_common::compress::decompress(&cb.content) + } else { + cb.content.to_vec() + }; + if let Ok(content) = String::from_utf8(content) { + self.handler.clipboard(content); + } + } + } #[cfg(target_os = "android")] crate::clipboard::handle_msg_multi_clipboards(_mcb); } From ee8cc0c06b86430ad274fdc36fbfded6ae8fb5ef Mon Sep 17 00:00:00 2001 From: eason <85663565+mango766@users.noreply.github.com> Date: Tue, 28 Apr 2026 11:04:29 +0800 Subject: [PATCH 240/277] fix(linux): prevent X11 BadWindow crash in get_focused_display (#14561) * fix(linux): prevent X11 BadWindow crash in get_focused_display When the active window is destroyed between xdo_get_active_window and xdo_get_window_location/xdo_get_window_size calls, the default X11 error handler terminates the process with a BadWindow error. This causes the rustdesk --server process to crash and the remote session to disconnect and reconnect every time the user closes a window. Install a custom X error handler around the xdo calls that catches BadWindow errors and returns gracefully instead of crashing. Fixes: https://github.com/rustdesk/rustdesk/issues/9003 Co-Authored-By: Claude (claude-opus-4-6) Signed-off-by: easonysliu * fix(linux): prevent BadWindow crash in focus display lookup Signed-off-by: fufesou --------- Signed-off-by: easonysliu Signed-off-by: fufesou Co-authored-by: easonysliu Co-authored-by: Claude (claude-opus-4-6) Co-authored-by: fufesou --- src/platform/linux.rs | 96 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 83 insertions(+), 13 deletions(-) diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 9493e1cae..7157da760 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -6,7 +6,7 @@ use hbb_common::{ anyhow::anyhow, bail, config::{keys::OPTION_ALLOW_LINUX_HEADLESS, Config}, - libc::{c_char, c_int, c_long, c_uint, c_void}, + libc::{c_char, c_int, c_long, c_uint, c_ulong, c_void}, log, message_proto::{DisplayInfo, Resolution}, regex::{Captures, Regex}, @@ -97,10 +97,55 @@ thread_local! { static DISPLAY: RefCell<*mut c_void> = RefCell::new(unsafe { XOpenDisplay(std::ptr::null())}); } +// X11 error event structure for the custom error handler. +// See: https://www.x.org/releases/current/doc/libX11/libX11/libX11.html#Using-the-Default-Error-Handlers +#[repr(C)] +struct XErrorEvent { + type_: c_int, + display: *mut c_void, // Display* + resourceid: c_ulong, // XID + serial: c_ulong, + error_code: u8, + request_code: u8, + minor_code: u8, +} + +type XErrorHandler = unsafe extern "C" fn(*mut c_void, *mut XErrorEvent) -> c_int; + +const X11_BAD_WINDOW: u8 = 3; +const XDO_SUCCESS: c_int = 0; +const XDO_ERROR: c_int = 1; + +/// Atomic flag set by the custom X error handler when a BadWindow error occurs. +static X_BAD_WINDOW_DETECTED: AtomicBool = AtomicBool::new(false); +static X_UNEXPECTED_ERROR_DETECTED: AtomicBool = AtomicBool::new(false); + +/// Custom X error handler that catches BadWindow errors (error_code == 3) instead of +/// letting the default handler terminate the process. +/// See issue: https://github.com/rustdesk/rustdesk/issues/9003 +unsafe extern "C" fn handle_x_error(_display: *mut c_void, event: *mut XErrorEvent) -> c_int { + if !event.is_null() && (*event).error_code == X11_BAD_WINDOW { + X_BAD_WINDOW_DETECTED.store(true, Ordering::SeqCst); + log::debug!("Caught X11 BadWindow error (suppressed), window was likely destroyed"); + return 0; + } + X_UNEXPECTED_ERROR_DETECTED.store(true, Ordering::SeqCst); + if !event.is_null() { + log::warn!( + "X11 error: error_code={}, request_code={}, minor_code={}", + (*event).error_code, + (*event).request_code, + (*event).minor_code, + ); + } + 0 +} + #[link(name = "X11")] extern "C" { fn XOpenDisplay(display_name: *const c_char) -> *mut c_void; // fn XCloseDisplay(d: *mut c_void) -> c_int; + fn XSetErrorHandler(handler: Option) -> Option; } #[link(name = "Xfixes")] @@ -231,25 +276,47 @@ pub fn get_focused_display(displays: Vec) -> Option { if libxdo_sys::xdo_get_active_window(*xdo as *const _, &mut window) != 0 { return; } - if libxdo_sys::xdo_get_window_location( + + // XSetErrorHandler is process-global, not scoped to this Display/thread. + // This path is currently called by the single window_focus service thread. + // While installed, this handler can still observe unrelated X11 errors from + // other threads; unexpected errors make this geometry query fail. + X_BAD_WINDOW_DETECTED.store(false, Ordering::SeqCst); + X_UNEXPECTED_ERROR_DETECTED.store(false, Ordering::SeqCst); + let prev_handler = XSetErrorHandler(Some(handle_x_error)); + + let loc_ret = libxdo_sys::xdo_get_window_location( *xdo as *const _, window, &mut x as _, &mut y as _, std::ptr::null_mut(), - ) != 0 - { - return; - } - if libxdo_sys::xdo_get_window_size( - *xdo as *const _, - window, - &mut width, - &mut height, - ) != 0 + ); + let size_ret = if loc_ret == XDO_SUCCESS { + libxdo_sys::xdo_get_window_size( + *xdo as *const _, + window, + &mut width, + &mut height, + ) + } else { + XDO_ERROR + }; + + // Do not call XSync(DISPLAY) here: DISPLAY is a separate + // XOpenDisplay() connection, while libxdo owns the Display* + // used by these geometry queries. These libxdo calls are + // synchronous XGetWindowAttributes-based queries, so the target + // BadWindow is expected to be delivered before the calls return. + XSetErrorHandler(prev_handler); + if X_BAD_WINDOW_DETECTED.load(Ordering::SeqCst) + || X_UNEXPECTED_ERROR_DETECTED.load(Ordering::SeqCst) + || loc_ret != XDO_SUCCESS + || size_ret != XDO_SUCCESS { return; } + let center_x = x + (width / 2) as c_int; let center_y = y + (height / 2) as c_int; res = displays.iter().position(|d| { @@ -2150,7 +2217,10 @@ pub fn clear_gnome_shortcuts_inhibitor_permission() -> ResultType<()> { || err_name == "org.freedesktop.DBus.Error.UnknownObject" || err_name == "org.freedesktop.DBus.Error.ServiceUnknown" { - log::info!("GNOME shortcuts inhibitor permission was not set ({})", err_name); + log::info!( + "GNOME shortcuts inhibitor permission was not set ({})", + err_name + ); Ok(()) } else { bail!("Failed to clear permission: {}", e) From 590296b297c7e5a718ba2c7792febd1e61a47032 Mon Sep 17 00:00:00 2001 From: Amirhosein Akhlaghpoor Date: Tue, 28 Apr 2026 07:03:41 +0000 Subject: [PATCH 241/277] fix: iPad mouse down detection for physical mouse input (#14515) * fix: iPad mouse down detection Signed-off-by: Amirhossein Akhlaghpour * fix(ipad): remove redundant check Signed-off-by: fufesou * fix(ipad): Simple refactor Signed-off-by: fufesou --------- Signed-off-by: Amirhossein Akhlaghpour Signed-off-by: fufesou Co-authored-by: fufesou --- flutter/lib/models/input_model.dart | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 427072677..6fdffd796 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -1495,6 +1495,16 @@ class InputModel { return false; } + /// iOS may emit a synthesized touch event after a real mouse click. + /// This helper ignores touch-down events that arrive shortly after a mouse down, + /// even when the position is far (e.g., near the top edge). + bool _shouldIgnoreTouchAfterMouse(int nowMs) { + if (!isIOS) return false; + const int kTouchAfterMouseWindowMs = 700; + final dt = nowMs - _lastMouseDownTimeMs; + return dt >= 0 && dt < kTouchAfterMouseWindowMs; + } + void onPointDownImage(PointerDownEvent e) { debugPrint("onPointDownImage ${e.kind}"); _stopFling = true; @@ -1507,6 +1517,9 @@ class InputModel { // Track mouse down events for duplicate detection on iOS. final nowMs = DateTime.now().millisecondsSinceEpoch; if (e.kind == ui.PointerDeviceKind.mouse) { + if (!isPhysicalMouse.value) { + isPhysicalMouse.value = true; + } _lastMouseDownTimeMs = nowMs; _lastMouseDownPos = e.position; } @@ -1516,6 +1529,10 @@ class InputModel { } if (e.kind != ui.PointerDeviceKind.mouse) { + // Ignore duplicate touch events that follow a recent mouse click (iOS Magic Mouse issue). + if (isPhysicalMouse.value && _shouldIgnoreTouchAfterMouse(nowMs)) { + return; + } if (isPhysicalMouse.value) { isPhysicalMouse.value = false; } From bfd31d21e4cbe8e78d750f8c0a0efbd2b3b0af1b Mon Sep 17 00:00:00 2001 From: KaneBarns <44869236+KaneBarns@users.noreply.github.com> Date: Tue, 28 Apr 2026 09:08:10 +0200 Subject: [PATCH 242/277] Update build.py (#11341) --- build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.py b/build.py index ce9a09ef6..5c53e4fc8 100755 --- a/build.py +++ b/build.py @@ -512,7 +512,7 @@ def main(): system2('pip3 install -r requirements.txt') system2( f'python3 ./generate.py -f ../../{res_dir} -o . -e ../../{res_dir}/rustdesk-{version}-win7-install.exe') - system2('mv ../../{res_dir}/rustdesk-{version}-win7-install.exe ../..') + system2(f'mv ../../{res_dir}/rustdesk-{version}-win7-install.exe ../..') elif os.path.isfile('/usr/bin/pacman'): # pacman -S -needed base-devel system2("sed -i 's/pkgver=.*/pkgver=%s/g' res/PKGBUILD" % version) From d4a1430c27e4d07cc4e37f737a256b23eaf52cd5 Mon Sep 17 00:00:00 2001 From: orbisai0security Date: Wed, 29 Apr 2026 10:45:21 +0530 Subject: [PATCH 243/277] fix: V-002 security vulnerability (#14924) Automated security fix generated by Orbis Security AI --- libs/clipboard/src/windows/wf_cliprdr.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/clipboard/src/windows/wf_cliprdr.c b/libs/clipboard/src/windows/wf_cliprdr.c index e1856863e..95d1d1a5c 100644 --- a/libs/clipboard/src/windows/wf_cliprdr.c +++ b/libs/clipboard/src/windows/wf_cliprdr.c @@ -624,6 +624,7 @@ void CliprdrStream_Delete(CliprdrStream *instance) if (instance) { free(instance->iStream.lpVtbl); + instance->iStream.lpVtbl = NULL; free(instance); } } @@ -2160,7 +2161,7 @@ static BOOL wf_cliprdr_add_to_file_arrays(wfClipboard *clipboard, WCHAR *full_fi return FALSE; /* add to name array */ - clipboard->file_names[clipboard->nFiles] = (LPWSTR)malloc(MAX_PATH * 2); + clipboard->file_names[clipboard->nFiles] = (LPWSTR)malloc((size_t)MAX_PATH * sizeof(WCHAR)); if (!clipboard->file_names[clipboard->nFiles]) return FALSE; From 383a5c34781523c9b3ebdf9db39d6a19501f1847 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 2 May 2026 00:44:22 +0800 Subject: [PATCH 244/277] feat: option, enable-privacy-mode & enable-perm-change-in-accept-window (#14875) * feat: option, privacy mode Signed-off-by: fufesou * feat(privacy mode): update libs/hbb_common Signed-off-by: fufesou * feat(privacy mode): turn off on disable privacy mode Signed-off-by: fufesou * feat(privacy mode): better check if supported Signed-off-by: fufesou * feat(option): enable perm change in accept window Signed-off-by: fufesou --------- Signed-off-by: fufesou --- flutter/lib/common/widgets/toolbar.dart | 36 +++++++-- flutter/lib/consts.dart | 3 + .../desktop/pages/desktop_setting_page.dart | 4 + flutter/lib/desktop/pages/server_page.dart | 41 +++++++++- .../lib/desktop/widgets/remote_toolbar.dart | 6 +- flutter/lib/mobile/pages/remote_page.dart | 3 +- flutter/lib/mobile/pages/server_page.dart | 51 ++++++++---- flutter/lib/models/server_model.dart | 20 ++++- flutter/lib/web/bridge.dart | 2 +- libs/hbb_common | 2 +- src/client/io_loop.rs | 3 + src/flutter_ffi.rs | 45 ++++++++++- src/ipc.rs | 1 + src/lang/ar.rs | 1 + src/lang/be.rs | 1 + src/lang/bg.rs | 1 + src/lang/ca.rs | 1 + src/lang/cn.rs | 1 + src/lang/cs.rs | 1 + src/lang/da.rs | 1 + src/lang/de.rs | 1 + src/lang/el.rs | 1 + src/lang/eo.rs | 1 + src/lang/es.rs | 1 + src/lang/et.rs | 1 + src/lang/eu.rs | 1 + src/lang/fa.rs | 1 + src/lang/fi.rs | 1 + src/lang/fr.rs | 1 + src/lang/ge.rs | 1 + src/lang/gu.rs | 1 + src/lang/he.rs | 1 + src/lang/hr.rs | 1 + src/lang/hu.rs | 1 + src/lang/id.rs | 1 + src/lang/it.rs | 1 + src/lang/ja.rs | 1 + src/lang/ko.rs | 1 + src/lang/kz.rs | 1 + src/lang/lt.rs | 1 + src/lang/lv.rs | 1 + src/lang/nb.rs | 1 + src/lang/nl.rs | 1 + src/lang/pl.rs | 1 + src/lang/pt_PT.rs | 1 + src/lang/ptbr.rs | 1 + src/lang/ro.rs | 1 + src/lang/ru.rs | 1 + src/lang/sc.rs | 1 + src/lang/sk.rs | 1 + src/lang/sl.rs | 1 + src/lang/sq.rs | 1 + src/lang/sr.rs | 1 + src/lang/sv.rs | 1 + src/lang/ta.rs | 1 + src/lang/template.rs | 1 + src/lang/th.rs | 1 + src/lang/tr.rs | 1 + src/lang/tw.rs | 1 + src/lang/uk.rs | 1 + src/lang/vi.rs | 1 + src/server/connection.rs | 78 +++++++++++++++++-- src/ui.rs | 6 ++ src/ui/cm.css | 11 +++ src/ui/cm.rs | 14 +++- src/ui/cm.tis | 39 ++++++++-- src/ui/header.tis | 2 +- src/ui/index.tis | 1 + src/ui/remote.tis | 2 + src/ui_cm_interface.rs | 76 ++++++++++++++++-- 70 files changed, 437 insertions(+), 57 deletions(-) diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index 1a6160324..2e7247d95 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -759,9 +759,18 @@ List toolbarPrivacyMode( final ffiModel = ffi.ffiModel; final pi = ffiModel.pi; final sessionId = ffi.sessionId; + final hasPrivacyModePermission = ffiModel.permissions['privacy_mode'] != false; + + // Backend revocation already attempts to turn privacy mode off. + // Still keep this menu when privacy mode is active, so users can turn it off + // if there is a sync delay, version mismatch, or off attempt failure. + if (!hasPrivacyModePermission && privacyModeState.isEmpty) { + return []; // No permission and not active, hide options. + } getDefaultMenu(Future Function(SessionID sid, String opt) toggleFunc) { - final enabled = !ffi.ffiModel.viewOnly; + final enabled = + !ffiModel.viewOnly && (hasPrivacyModePermission || privacyModeState.isNotEmpty); return TToggleMenu( value: privacyModeState.isNotEmpty, onChanged: enabled @@ -810,18 +819,29 @@ List toolbarPrivacyMode( }) ]; } else { - return privacyModeImpls.map((e) { + final visibleImpls = hasPrivacyModePermission + ? privacyModeImpls + : privacyModeImpls.where((e) { + final implKey = (e as List)[0] as String; + return privacyModeState.value == implKey; + }).toList(); + return visibleImpls.map((e) { final implKey = (e as List)[0] as String; final implName = (e)[1] as String; + final enabled = !ffiModel.viewOnly && + (hasPrivacyModePermission || privacyModeState.value == implKey); return TToggleMenu( child: Text(translate(implName)), value: privacyModeState.value == implKey, - onChanged: (value) { - if (value == null) return; - togglePrivacyModeTime = DateTime.now(); - bind.sessionTogglePrivacyMode( - sessionId: sessionId, implKey: implKey, on: value); - }); + onChanged: enabled + ? (value) { + if (value == null) return; + if (value && !hasPrivacyModePermission) return; + togglePrivacyModeTime = DateTime.now(); + bind.sessionTogglePrivacyMode( + sessionId: sessionId, implKey: implKey, on: value); + } + : null); }).toList(); } } diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 51c08cf33..832b96d24 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -114,6 +114,9 @@ const String kOptionTerminalPersistent = "terminal-persistent"; const String kOptionEnableTunnel = "enable-tunnel"; const String kOptionEnableRemoteRestart = "enable-remote-restart"; const String kOptionEnableBlockInput = "enable-block-input"; +const String kOptionEnablePrivacyMode = "enable-privacy-mode"; +const String kOptionEnablePermChangeInAcceptWindow = + "enable-perm-change-in-accept-window"; const String kOptionAllowRemoteConfigModification = "allow-remote-config-modification"; const String kOptionVerificationMethod = "verification-method"; diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index d118b6793..2841c1d27 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -1062,6 +1062,10 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { _OptionCheckBox(context, 'Enable blocking user input', kOptionEnableBlockInput, enabled: enabled, fakeValue: fakeValue), + if (bind.mainSupportedPrivacyModeImpls() != '[]') + _OptionCheckBox( + context, 'Enable privacy mode', kOptionEnablePrivacyMode, + enabled: enabled, fakeValue: fakeValue), _OptionCheckBox(context, 'Enable remote configuration modification', kOptionAllowRemoteConfigModification, enabled: enabled, fakeValue: fakeValue), diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 7d48452a8..8bd7df08b 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -610,19 +610,24 @@ class _PrivilegeBoard extends StatefulWidget { class _PrivilegeBoardState extends State<_PrivilegeBoard> { late final client = widget.client; Widget buildPermissionIcon(bool enabled, IconData iconData, - Function(bool)? onTap, String tooltipText) { + Function(bool)? onTap, String tooltipText, + {required bool canModify}) { return Tooltip( message: "$tooltipText: ${enabled ? "ON" : "OFF"}", waitDuration: Duration.zero, child: Container( decoration: BoxDecoration( - color: enabled ? MyTheme.accent : Colors.grey[700], + color: enabled + ? (canModify ? MyTheme.accent : MyTheme.accent.withOpacity(0.6)) + : Colors.grey[700], borderRadius: BorderRadius.circular(10.0), ), padding: EdgeInsets.all(8.0), child: InkWell( - onTap: () => - checkClickTime(widget.client.id, () => onTap?.call(!enabled)), + onTap: canModify + ? () => + checkClickTime(widget.client.id, () => onTap?.call(!enabled)) + : null, child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ @@ -643,6 +648,9 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { Widget build(BuildContext context) { final crossAxisCount = 4; final spacing = 10.0; + final canModifyPermission = + bind.mainGetBuildinOption(key: kOptionEnablePermChangeInAcceptWindow) != + 'N'; return Container( width: double.infinity, height: 160.0, @@ -689,6 +697,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { }); }, translate('Enable audio'), + canModify: canModifyPermission, ), buildPermissionIcon( client.recording, @@ -703,6 +712,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { }); }, translate('Enable recording session'), + canModify: canModifyPermission, ), ] : [ @@ -719,6 +729,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { }); }, translate('Enable keyboard/mouse'), + canModify: canModifyPermission, ), buildPermissionIcon( client.clipboard, @@ -733,6 +744,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { }); }, translate('Enable clipboard'), + canModify: canModifyPermission, ), buildPermissionIcon( client.audio, @@ -747,6 +759,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { }); }, translate('Enable audio'), + canModify: canModifyPermission, ), buildPermissionIcon( client.file, @@ -761,6 +774,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { }); }, translate('Enable file copy and paste'), + canModify: canModifyPermission, ), buildPermissionIcon( client.restart, @@ -775,6 +789,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { }); }, translate('Enable remote restart'), + canModify: canModifyPermission, ), buildPermissionIcon( client.recording, @@ -789,6 +804,7 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { }); }, translate('Enable recording session'), + canModify: canModifyPermission, ), // only windows support block input if (isWindows) @@ -805,6 +821,23 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { }); }, translate('Enable blocking user input'), + canModify: canModifyPermission, + ), + if (bind.mainSupportedPrivacyModeImpls() != '[]') + buildPermissionIcon( + client.privacyMode, + Icons.visibility_off, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "privacy_mode", + enabled: enabled); + setState(() { + client.privacyMode = enabled; + }); + }, + translate('Enable privacy mode'), + canModify: canModifyPermission, ) ], ), diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index ec05c987f..5da253e80 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -996,10 +996,10 @@ class _DisplayMenuState extends State<_DisplayMenu> { toggles(), ]; // privacy mode + final privacyModeState = PrivacyModeState.find(id); if (ffi.connType == ConnType.defaultConn && - ffiModel.keyboard && - pi.features.privacyMode) { - final privacyModeState = PrivacyModeState.find(id); + (pi.features.privacyMode || privacyModeState.isNotEmpty) && + (ffiModel.keyboard || privacyModeState.isNotEmpty)) { final privacyModeList = toolbarPrivacyMode(privacyModeState, context, id, ffi); if (privacyModeList.length == 1) { diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 9064c122b..74a5af45c 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -1183,7 +1183,8 @@ void showOptions( List privacyModeList = []; // privacy mode final privacyModeState = PrivacyModeState.find(id); - if (gFFI.ffiModel.keyboard && gFFI.ffiModel.pi.features.privacyMode) { + if ((gFFI.ffiModel.pi.features.privacyMode && gFFI.ffiModel.keyboard) || + privacyModeState.isNotEmpty) { privacyModeList = toolbarPrivacyMode(privacyModeState, context, id, gFFI); if (privacyModeList.length == 1) { displayToggles.add(privacyModeList[0]); diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index 2c8b0f2d6..cd3f97a53 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -583,9 +583,16 @@ class _PermissionCheckerState extends State { Widget build(BuildContext context) { final serverModel = Provider.of(context); final hasAudioPermission = androidVersion >= 30; - final hideStopService = - isAndroid && - bind.mainGetBuildinOption(key: kOptionHideStopService) == 'Y'; + final hideStopService = isAndroid && + bind.mainGetBuildinOption(key: kOptionHideStopService) == 'Y'; + final allowPermChangeInAcceptWindow = option2bool( + kOptionEnablePermChangeInAcceptWindow, + bind.mainGetBuildinOption( + key: kOptionEnablePermChangeInAcceptWindow, + )); + final permissionChangeLocked = isAndroid && + serverModel.clients.any((c) => !c.disconnected) && + !allowPermChangeInAcceptWindow; return PaddingCard( title: translate("Permissions"), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -608,13 +615,21 @@ class _PermissionCheckerState extends State { bind.mainGetLocalOption(key: "show-scam-warning") != "N" ? () => showScamWarning(context, serverModel) : serverModel.toggleService), - PermissionRow(translate("Input Control"), serverModel.inputOk, - serverModel.toggleInput), - PermissionRow(translate("Transfer file"), serverModel.fileOk, - serverModel.toggleFile), + PermissionRow( + translate("Input Control"), + serverModel.inputOk, + serverModel.toggleInput, + ), + PermissionRow( + translate("Transfer file"), + serverModel.fileOk, + serverModel.toggleFile, + enabled: !permissionChangeLocked, + ), hasAudioPermission ? PermissionRow(translate("Audio Capture"), serverModel.audioOk, - serverModel.toggleAudio) + serverModel.toggleAudio, + enabled: !permissionChangeLocked) : Row(children: [ Icon(Icons.info_outline).marginOnly(right: 15), Expanded( @@ -623,19 +638,25 @@ class _PermissionCheckerState extends State { style: const TextStyle(color: MyTheme.darkGray), )) ]), - PermissionRow(translate("Enable clipboard"), serverModel.clipboardOk, - serverModel.toggleClipboard), + PermissionRow( + translate("Enable clipboard"), + serverModel.clipboardOk, + serverModel.toggleClipboard, + enabled: !permissionChangeLocked, + ), ])); } } class PermissionRow extends StatelessWidget { - const PermissionRow(this.name, this.isOk, this.onPressed, {Key? key}) + const PermissionRow(this.name, this.isOk, this.onPressed, + {Key? key, this.enabled = true}) : super(key: key); final String name; final bool isOk; final VoidCallback onPressed; + final bool enabled; @override Widget build(BuildContext context) { @@ -644,9 +665,11 @@ class PermissionRow extends StatelessWidget { contentPadding: EdgeInsets.all(0), title: Text(name), value: isOk, - onChanged: (bool value) { - onPressed(); - }); + onChanged: enabled + ? (bool value) { + onPressed(); + } + : null); } } diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 78e334d4f..40c94fcf5 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -298,7 +298,7 @@ class ServerModel with ChangeNotifier { } toggleAudio() async { - if (clients.isNotEmpty) { + if (clients.any((c) => !c.disconnected)) { await showClientsMayNotBeChangedAlert(parent.target); } if (!_audioOk && !await AndroidPermissionManager.check(kRecordAudio)) { @@ -316,7 +316,7 @@ class ServerModel with ChangeNotifier { } toggleFile() async { - if (clients.isNotEmpty) { + if (clients.any((c) => !c.disconnected)) { await showClientsMayNotBeChangedAlert(parent.target); } if (!_fileOk && @@ -345,7 +345,7 @@ class ServerModel with ChangeNotifier { } toggleInput() async { - if (clients.isNotEmpty) { + if (clients.any((c) => !c.disconnected)) { await showClientsMayNotBeChangedAlert(parent.target); } if (_inputOk) { @@ -549,10 +549,19 @@ class ServerModel with ChangeNotifier { if (index < 0) { _clients.add(client); } else { + if (_clients[index].authorized) { + _clients[index].privacyMode = client.privacyMode; + notifyListeners(); + return; + } _clients[index].authorized = true; + _clients[index].privacyMode = client.privacyMode; } } else { - if (_clients.any((c) => c.id == client.id)) { + final index = _clients.indexWhere((c) => c.id == client.id); + if (index >= 0) { + _clients[index].privacyMode = client.privacyMode; + notifyListeners(); return; } _clients.add(client); @@ -818,6 +827,7 @@ class Client { bool restart = false; bool recording = false; bool blockInput = false; + bool privacyMode = false; bool disconnected = false; bool fromSwitch = false; bool inVoiceCall = false; @@ -846,6 +856,7 @@ class Client { restart = json['restart']; recording = json['recording']; blockInput = json['block_input']; + privacyMode = json['privacy_mode'] ?? privacyMode; disconnected = json['disconnected']; fromSwitch = json['from_switch']; inVoiceCall = json['in_voice_call']; @@ -870,6 +881,7 @@ class Client { data['restart'] = restart; data['recording'] = recording; data['block_input'] = blockInput; + data['privacy_mode'] = privacyMode; data['disconnected'] = disconnected; data['from_switch'] = fromSwitch; data['in_voice_call'] = inVoiceCall; diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index a3d93f88e..54e6a9a9b 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -1729,7 +1729,7 @@ class RustdeskImpl { } String mainSupportedPrivacyModeImpls({dynamic hint}) { - throw UnimplementedError("mainSupportedPrivacyModeImpls"); + return '[]'; } String mainSupportedInputSource({dynamic hint}) { diff --git a/libs/hbb_common b/libs/hbb_common index 87b11a795..3e31a9493 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 87b11a795964b00deded250657a63626f2c1efa0 +Subproject commit 3e31a94939e026ab2c05d21a2c436960aa9bfea8 diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index e8afa8e01..78ba9ebc6 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1797,6 +1797,9 @@ impl Remote { Ok(Permission::BlockInput) => { self.handler.set_permission("block_input", p.enabled); } + Ok(Permission::PrivacyMode) => { + self.handler.set_permission("privacy_mode", p.enabled); + } _ => {} } } diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 1ee13f4df..3f97df078 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -972,6 +972,27 @@ pub fn main_show_option(_key: String) -> SyncReturn { } pub fn main_set_option(key: String, value: String) { + #[cfg(target_os = "android")] + { + let is_permission_option = key.eq(config::keys::OPTION_ENABLE_CLIPBOARD) + || key.eq(config::keys::OPTION_ENABLE_FILE_TRANSFER) + || key.eq(config::keys::OPTION_ENABLE_AUDIO); + let allow_perm_change_in_accept_window = config::option2bool( + config::keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW, + &crate::get_builtin_option(config::keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW), + ); + if is_permission_option + && !allow_perm_change_in_accept_window + && crate::ui_cm_interface::has_active_clients() + { + log::info!( + "blocked main_set_option by policy, key={}, value={}", + key, + value + ); + return; + } + } #[cfg(target_os = "android")] if key.eq(config::keys::OPTION_ENABLE_KEYBOARD) { crate::ui_cm_interface::switch_permission_all( @@ -1019,7 +1040,29 @@ pub fn main_get_options_sync() -> SyncReturn { } pub fn main_set_options(json: String) { - let map: HashMap = serde_json::from_str(&json).unwrap_or(HashMap::new()); + let mut map: HashMap = serde_json::from_str(&json).unwrap_or(HashMap::new()); + #[cfg(target_os = "android")] + { + let allow_perm_change_in_accept_window = config::option2bool( + config::keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW, + &crate::get_builtin_option(config::keys::OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW), + ); + if !allow_perm_change_in_accept_window && crate::ui_cm_interface::has_active_clients() { + for key in [ + config::keys::OPTION_ENABLE_CLIPBOARD, + config::keys::OPTION_ENABLE_FILE_TRANSFER, + config::keys::OPTION_ENABLE_AUDIO, + ] { + if let Some(value) = map.remove(key) { + log::info!( + "blocked main_set_options item by policy, key={}, value={}", + key, + value + ); + } + } + } + } if !map.is_empty() { set_options(map) } diff --git a/src/ipc.rs b/src/ipc.rs index 099c24d34..e6d4fc834 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -237,6 +237,7 @@ pub enum Data { restart: bool, recording: bool, block_input: bool, + privacy_mode: bool, from_switch: bool, }, ChatMessage { diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 6d48e34ee..4113c1391 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "اسم العرض"), ("password-hidden-tip", "كلمة المرور مخفية"), ("preset-password-in-use-tip", "كلمة المرور المحددة مسبقًا قيد الاستخدام"), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 5ea7c3351..1a3260c5a 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Імя для адлюстравання"), ("password-hidden-tip", "Зададзены пастаянны пароль (скрыты)."), ("preset-password-in-use-tip", "Пададзены пароль цяпер выкарыстоўваецца"), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 218070291..17a89ce07 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 2f1cc8734..799ca951f 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 75d16ff92..1ff10c49d 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "显示名称"), ("password-hidden-tip", "永久密码已设置(已隐藏)"), ("preset-password-in-use-tip", "当前使用预设密码"), + ("Enable privacy mode", "允许隐私模式"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 7b3dc7908..2b9c6219e 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 06ad254c7..7410124df 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 39e077348..7d18cd7a1 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Anzeigename"), ("password-hidden-tip", "Ein permanentes Passwort wurde festgelegt (ausgeblendet)."), ("preset-password-in-use-tip", "Das voreingestellte Passwort wird derzeit verwendet."), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 38e11bfce..0633889a7 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Εμφανιζόμενο όνομα"), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 921f79612..16d43c9b4 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 0f49079a2..2e543c25e 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index d65cd31c5..a00c312b8 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index f12ecf371..aaf8a8be8 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index 5f6d5f005..d34e4239e 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fi.rs b/src/lang/fi.rs index 43c033a11..1bddd39d1 100644 --- a/src/lang/fi.rs +++ b/src/lang/fi.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 8ad712f1e..ab6ed2e76 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Nom d’affichage"), ("password-hidden-tip", "Le mot de passe permanent est défini (masqué)."), ("preset-password-in-use-tip", "Le mot de passe prédéfini est actuellement utilisé."), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ge.rs b/src/lang/ge.rs index dc78bc0d9..fba2fd83d 100644 --- a/src/lang/ge.rs +++ b/src/lang/ge.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gu.rs b/src/lang/gu.rs index 39c45597c..8b8568c85 100644 --- a/src/lang/gu.rs +++ b/src/lang/gu.rs @@ -742,5 +742,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "ડિસ્પ્લે નામ"), ("password-hidden-tip", "સુરક્ષા માટે પાસવર્ડ છુપાવેલ છે."), ("preset-password-in-use-tip", "પ્રીસેટ પાસવર્ડ વપરાશમાં છે."), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 741805e25..682ee0c46 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 2d596bacc..505b01df9 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 2ba49a0cf..7f9b3299e 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Kijelző név"), ("password-hidden-tip", "Állandó jelszó lett beállítva (rejtett)."), ("preset-password-in-use-tip", "Jelenleg az alapértelmezett jelszót használja."), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 356a9ee2d..bbd95e79a 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 1b6e49691..b83ee01ed 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Visualizza nome"), ("password-hidden-tip", "È impostata una password permanente (nascosta)."), ("preset-password-in-use-tip", "È attualmente in uso la password preimpostata."), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 56faba383..20caca0a7 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "表示名"), ("password-hidden-tip", "永続的なパスワードが設定されています (非表示)"), ("preset-password-in-use-tip", "プリセットパスワードが現在使用されています"), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 7cc0c9067..7b3ffd98e 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "표시 이름"), ("password-hidden-tip", "영구 비밀번호가 설정되었습니다 (숨김)."), ("preset-password-in-use-tip", "현재 사전 설정된 비밀번호가 사용 중입니다."), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index e943ff4cd..a2a1624f7 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index a4f39f1e4..82422c30a 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 838984207..906d056bd 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index d9cf6ad38..5795b9eeb 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 6d140daad..833c947cf 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Naam Weergeven"), ("password-hidden-tip", "Er is een permanent wachtwoord ingesteld (verborgen)."), ("preset-password-in-use-tip", "Het basis wachtwoord is momenteel in gebruik."), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 2000de2c8..972afc170 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Nazwa wyświetlana"), ("password-hidden-tip", "Ustawiono (ukryto) stare hasło."), ("preset-password-in-use-tip", "Obecnie używane jest hasło domyślne."), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 0cdcf93b4..899c8da71 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index f9bae32b1..4eb2c1544 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 7ace3f736..45b22684e 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Nume afișat"), ("password-hidden-tip", "Parola este ascunsă din motive de securitate. Fă clic pe pictograma ochiului pentru a o afișa."), ("preset-password-in-use-tip", "Se folosește o parolă prestabilită. Se recomandă setarea unei parole personalizate pentru securitate sporită."), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 14bc96390..20000cd26 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Отображаемое имя"), ("password-hidden-tip", "Установлен постоянный пароль (скрытый)."), ("preset-password-in-use-tip", "Установленный пароль сейчас используется."), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index f2c4fbfa2..68ce541f2 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index d0e99b2a4..6b4e16688 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index aef6b7c66..3f35dea88 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 5f9d5505b..f7f6c16d4 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 19ae6896f..bedbe4856 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 7ad257fcb..eda7851c1 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs index 2cee45268..6e5652560 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index ff755768c..5e25801d2 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 2d3eb1d34..c2d058c98 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 5acb15221..40eb561ed 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Görünen Ad"), ("password-hidden-tip", "Şifre gizli"), ("preset-password-in-use-tip", "Önceden ayarlanmış şifre kullanılıyor"), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 5211cc92b..b23b84949 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "顯示名稱"), ("password-hidden-tip", "固定密碼已設定(已隱藏)"), ("preset-password-in-use-tip", "目前正在使用預設密碼"), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 2594b7cc3..3e1c4f25e 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vi.rs b/src/lang/vi.rs index 6939b2ea1..3fadb0efc 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -743,5 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", ""), ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), + ("Enable privacy mode", ""), ].iter().cloned().collect(); } diff --git a/src/server/connection.rs b/src/server/connection.rs index 8b4eb0c48..bd5327bb2 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -241,6 +241,7 @@ pub struct Connection { restart: bool, recording: bool, block_input: bool, + privacy_mode: bool, control_permissions: Option, last_test_delay: Option, network_delay: u32, @@ -431,6 +432,7 @@ impl Connection { restart: Self::permission(keys::OPTION_ENABLE_REMOTE_RESTART, &control_permissions), recording: Self::permission(keys::OPTION_ENABLE_RECORD_SESSION, &control_permissions), block_input: Self::permission(keys::OPTION_ENABLE_BLOCK_INPUT, &control_permissions), + privacy_mode: Self::permission(keys::OPTION_ENABLE_PRIVACY_MODE, &control_permissions), control_permissions, last_test_delay: None, network_delay: 0, @@ -527,6 +529,9 @@ impl Connection { if !conn.block_input { conn.send_permission(Permission::BlockInput, false).await; } + if !conn.privacy_mode { + conn.send_permission(Permission::PrivacyMode, false).await; + } let mut test_delay_timer = crate::rustdesk_interval(time::interval_at(Instant::now(), TEST_DELAY_TIMEOUT)); let mut last_recv_time = Instant::now(); @@ -674,6 +679,46 @@ impl Connection { } else if &name == "block_input" { conn.block_input = enabled; conn.send_permission(Permission::BlockInput, enabled).await; + } else if &name == "privacy_mode" { + // Keep permission state and runtime state consistent: + // when revoking the permission, try to leave privacy mode first. + // Otherwise we could end up in an inconsistent state where + // permission looks disabled while privacy mode is still active. + if !enabled && privacy_mode::is_in_privacy_mode() { + if let Some(conn_id) = privacy_mode::get_privacy_mode_conn_id() { + if conn_id == conn.inner.id() { + let impl_key = + privacy_mode::get_cur_impl_key().unwrap_or_default(); + let turn_off_res = + privacy_mode::turn_off_privacy(conn_id, None); + match turn_off_res { + Some(Ok(_)) => { + let msg_out = crate::common::make_privacy_mode_msg( + back_notification::PrivacyModeState::PrvOffByPeer, + impl_key.clone(), + ); + conn.send(msg_out).await; + } + _ => { + let msg_out = Self::turn_off_privacy_result_to_msg( + turn_off_res, + impl_key, + ); + conn.send(msg_out).await; + // Turn-off failed, so revert CM's optimistic toggle + // and keep the previous permission value. + conn.send_to_cm(ipc::Data::SwitchPermission { + name: "privacy_mode".to_owned(), + enabled: conn.privacy_mode, + }); + continue; + } + } + } + } + } + conn.privacy_mode = enabled; + conn.send_permission(Permission::PrivacyMode, enabled).await; } } ipc::Data::RawMessage(bytes) => { @@ -978,7 +1023,7 @@ impl Connection { if let Some(video_privacy_conn_id) = privacy_mode::get_privacy_mode_conn_id() { if video_privacy_conn_id == id { - let _ = Self::turn_off_privacy_to_msg(id); + let _ = Self::turn_off_privacy_to_msg(id, String::new()); } } #[cfg(all(feature = "flutter", feature = "plugin_framework"))] @@ -1900,6 +1945,7 @@ impl Connection { restart: self.restart, recording: self.recording, block_input: self.block_input, + privacy_mode: self.privacy_mode, from_switch: self.from_switch, }); } @@ -2175,6 +2221,7 @@ impl Connection { keys::OPTION_ENABLE_REMOTE_RESTART => Some(Permission::restart), keys::OPTION_ENABLE_RECORD_SESSION => Some(Permission::recording), keys::OPTION_ENABLE_BLOCK_INPUT => Some(Permission::block_input), + keys::OPTION_ENABLE_PRIVACY_MODE => Some(Permission::privacy_mode), _ => None, }; if let Some(permission) = permission { @@ -4145,6 +4192,15 @@ impl Connection { } async fn turn_on_privacy(&mut self, impl_key: String) { + if !self.is_authed_remote_conn() || !self.privacy_mode { + let msg_out = crate::common::make_privacy_mode_msg( + back_notification::PrivacyModeState::PrvOnFailedDenied, + impl_key, + ); + self.send(msg_out).await; + return; + } + let msg_out = if !privacy_mode::is_privacy_mode_supported() { crate::common::make_privacy_mode_msg_with_details( back_notification::PrivacyModeState::PrvNotSupported, @@ -4186,7 +4242,7 @@ impl Connection { "Check privacy mode failed: {}, turn off privacy mode.", &err_msg ); - let _ = Self::turn_off_privacy_to_msg(self.inner.id); + let _ = Self::turn_off_privacy_to_msg(self.inner.id, String::new()); crate::common::make_privacy_mode_msg_with_details( back_notification::PrivacyModeState::PrvOnFailed, err_msg, @@ -4205,6 +4261,7 @@ impl Connection { if privacy_mode::is_in_privacy_mode() { let _ = Self::turn_off_privacy_to_msg( privacy_mode::INVALID_PRIVACY_MODE_CONN_ID, + String::new(), ); } crate::common::make_privacy_mode_msg_with_details( @@ -4232,14 +4289,23 @@ impl Connection { impl_key, ) } else { - Self::turn_off_privacy_to_msg(self.inner.id) + Self::turn_off_privacy_to_msg(self.inner.id, impl_key) }; self.send(msg_out).await; } - pub fn turn_off_privacy_to_msg(_conn_id: i32) -> Message { - let impl_key = "".to_owned(); - match privacy_mode::turn_off_privacy(_conn_id, None) { + pub fn turn_off_privacy_to_msg(_conn_id: i32, impl_key: String) -> Message { + Self::turn_off_privacy_result_to_msg( + privacy_mode::turn_off_privacy(_conn_id, None), + impl_key, + ) + } + + fn turn_off_privacy_result_to_msg( + turn_off_res: Option>, + impl_key: String, + ) -> Message { + match turn_off_res { Some(Ok(_)) => crate::common::make_privacy_mode_msg( back_notification::PrivacyModeState::PrvOffSucceeded, impl_key, diff --git a/src/ui.rs b/src/ui.rs index 154319ce4..6d0d0927a 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -372,6 +372,11 @@ impl UI { is_installed() } + fn get_supported_privacy_mode_impls(&self) -> String { + serde_json::to_string(&crate::privacy_mode::get_supported_privacy_mode_impl()) + .unwrap_or_default() + } + fn is_root(&self) -> bool { is_root() } @@ -752,6 +757,7 @@ impl sciter::EventHandler for UI { fn get_icon(); fn install_me(String, String); fn is_installed(); + fn get_supported_privacy_mode_impls(); fn is_root(); fn is_release(); fn set_socks(String, String, String); diff --git a/src/ui/cm.css b/src/ui/cm.css index ba6de887b..3ac6c7be3 100644 --- a/src/ui/cm.css +++ b/src/ui/cm.css @@ -93,6 +93,13 @@ div.permissions > div:active { opacity: 0.5; } +div.permissions.locked, +div.permissions.locked *, +div.permissions.locked > div:active { + cursor: default !important; + opacity: 1; +} + icon.keyboard { background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAgVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9d3yJTAAAAKnRSTlMA0Gd/0y8ILZgbJffDPUwV2nvzt+TMqZxyU7CMb1pYQyzsvKunkXE4AwJnNC24AAAA+0lEQVQ4y83O2U7DMBCF4ZMxk9rZk26kpQs7nPd/QJy4EiLbLf01N5Y/2YP/qxDFQvGB5NPC/ZpVnfJx4b5xyGfF95rkHvNCWH1u+N6J6T0sC7gqRy8uGPfBLEbozPXUjlkQKwGaFPNizwQbwkx0TDvhCii34ExZCSQVBdzIOEOyeclSHgBGXkpeygXSQgStACtWx4Z8rr8COHOvfEP/IbbsQAToFUAAV1M408IIjIGYAPoCSNRP7DQutfQTqxuAiH7UUg1FaJR2AGrrx52sK2ye28LZ0wBAEyR6y8X+NADhm1B4fgiiHXbRrTrxpwEY9RdM9wsepnvFHfUDwYEeiwAJr/gAAAAASUVORK5CYII='); } @@ -121,6 +128,10 @@ icon.block_input { background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAjdJREFUWEe1V8tNAzEQfXOHAx2QG0UgQSqBFIIgHdABoQqOhBq4cCMlcMh90FvZq/HEXtvJxlKUZNceP783no+gY6jqNYBHAHcA+JufXTDBb37eRWTbalZqE82mz7W55v0ABMBGRCLA7PJJAKr6AiC3sT11NHyf2SEyQjvtAMKp3wBYo9VTGbYegjxxU65d5tg4YEBVbwF8ALgw2lLX4in80QqyZUEkAMLCb7P5n4hcdWifTA32Pg0bByA8AE4+oL3n9A1s7ERkEeeNAJzD/QC4OVaCAgjrU7wdK86zAHREJSKqyvvORRxVb67JFOT4NfYGpxwAqCo34oYcKxHZhOdzg7D2BhYigHj6RJ+5QbjrPezlqR61sZTOKYfztSUBWPoXpdA5FwjnC2sCGK+eiNRC8yw+oap0RiayLQHEPwf65zx7DibMoXcEEB0wq/85QJQAbEVkWbvP8f0pTFi/65ZgjtuRyJ7QYWL0OZnwTmiLDobH5nLqGDlUlcmON49jQwnsg/Wxma/VJ1zcGQIR7+OYJGyqbJWhhwlDPxh3JpNRL4Ba7nAsJckoYaFUv7UCyslBvQ3TNDWEfVsPJGH2FCkKTPAxD8ox+poFwJfZqqX15H6eYyK+TgJeriidLCJ7wAQHZ4Udy7u9iFxaG7mynEx4EF1leZDANzV7AE8i8joJICz2cvBxbExIYTZYTTQmxTxTzP+VnvC8rZlLOLEj7m5OW6JqtTs2US6247Hvy7XnX0OV05FP/gHde5fLZaGS8AAAAABJRU5ErkJggg=='); } +icon.privacy_mode { + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAB7UlEQVR4AdyTrVYDMRCFuyjqiiuuOJA46sCVR6jDgQTXN+CgQIJCgkOCA0cduOLAgaOOuuW7czYhyWY5FcXQc28n85O5m9nsUuuPf/9IoCzLLnxd9MTCET3SvNckQnwL7lfcpnYueIGiKNbY8QYjERo+wZK4HuAcK94rVvGSWCO8gCqKjAixTXLPsAl7ldBxriASqAo6lfUnqUTaWAP5FajTYjxGCNXeYSRAwSflToBlKxSZKSCiMoUa6Uh+QNW/B37LC9D8lkTYHNegTf7JqNP8b5RB5AT7AkPoNqqXxUyATT28AUzhRuFFaLpDUYc9V1ihr7+EA/JdxUyAxQTWQDM3CuVSEWugGiUztJ5OIJPPhlKRbFEVXJZ1Anph8iNyTCsieA0dvIgCQY3ckBtyTIBjfuDcwRR2TPJDElkRcrpd6XcyJm7X2ATY3CKwi1UxxkNPeyiP/BAa8LVZObtdBMOPcYbvX7wXYJNE2lidBuNxyhgm0I1LCdcgFXmguXqoxhgJKELBKvYMhljH+ULEwDr8mEIRXWHSP6gJKIXIESxYh3PHzWJK1IuwjpAVcBWIhHPX0x2QE/vkHGofIzUevwr4KhZ003wvsOKYkAcxXfPoxbvk3AJuQ5MNRNwFsNKFCaibRGB0CxcqIJGU3wAAAP//8GtoDAAAAAZJREFUAwCJJuAxFVNbWwAAAABJRU5ErkJggg=='); +} + div.outer_buttons { flow:vertical; border-spacing:8; diff --git a/src/ui/cm.rs b/src/ui/cm.rs index 8eb8f494e..4a68a571d 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -36,7 +36,8 @@ impl InvokeUiCM for SciterHandler { client.file, client.restart, client.recording, - client.block_input + client.block_input, + client.privacy_mode ), ); } @@ -157,9 +158,18 @@ impl SciterConnectionManager { crate::ui_interface::get_option(key) } + fn get_builtin_option(&self, key: String) -> String { + crate::ui_interface::get_builtin_option(&key) + } + fn hide_cm(&self) -> bool { *crate::ui::cm::HIDE_CM.lock().unwrap() } + + fn get_supported_privacy_mode_impls(&self) -> String { + serde_json::to_string(&crate::privacy_mode::get_supported_privacy_mode_impl()) + .unwrap_or_default() + } } impl sciter::EventHandler for SciterConnectionManager { @@ -181,6 +191,8 @@ impl sciter::EventHandler for SciterConnectionManager { fn can_elevate(); fn elevate_portable(i32); fn get_option(String); + fn get_builtin_option(String); fn hide_cm(); + fn get_supported_privacy_mode_impls(); } } diff --git a/src/ui/cm.tis b/src/ui/cm.tis index a06fb9ff8..f306e9032 100644 --- a/src/ui/cm.tis +++ b/src/ui/cm.tis @@ -4,6 +4,9 @@ var body; var connections = []; var show_chat = false; var show_elevation = true; +var is_privacy_mode_supported = handler.get_supported_privacy_mode_impls() != '[]'; +var allow_perm_change_in_accept_window = + handler.get_builtin_option('enable-perm-change-in-accept-window') != 'N'; var svg_elevate = ; var hide_cm = undefined; @@ -35,6 +38,7 @@ class Body: Reactor.Component me.sendMsg(msg); }; var right_style = show_chat ? "" : "display: none"; + var permissions_locked = !allow_perm_change_in_accept_window; var disconnected = c.disconnected; var show_elevation_btn = handler.can_elevate() && show_elevation && !c.is_file_transfer && !c.is_view_camera && !c.is_terminal && c.port_forward.length == 0; var show_accept_btn = handler.get_option('approve-mode') != 'password'; @@ -58,15 +62,16 @@ class Body: Reactor.Component

    {c.is_file_transfer || c.is_terminal || c.port_forward || disconnected ? "" :
    {translate('Permissions')}
    } - {c.is_file_transfer || c.is_terminal || c.port_forward || disconnected ? "" :
    + {c.is_file_transfer || c.is_terminal || c.port_forward || disconnected ? "" :
    -
    +
    +
    } {c.is_file_transfer ?
    {translate('Transfer file')}
    : ""} @@ -103,6 +108,7 @@ class Body: Reactor.Component } event click $(icon.keyboard) (e) { + if (!allow_perm_change_in_accept_window) return; var { cid, connection } = this; checkClickTime(function() { connection.keyboard = !connection.keyboard; @@ -112,6 +118,7 @@ class Body: Reactor.Component } event click $(icon.clipboard) { + if (!allow_perm_change_in_accept_window) return; var { cid, connection } = this; checkClickTime(function() { connection.clipboard = !connection.clipboard; @@ -121,6 +128,7 @@ class Body: Reactor.Component } event click $(icon.audio) { + if (!allow_perm_change_in_accept_window) return; var { cid, connection } = this; checkClickTime(function() { connection.audio = !connection.audio; @@ -130,6 +138,7 @@ class Body: Reactor.Component } event click $(icon.file) { + if (!allow_perm_change_in_accept_window) return; var { cid, connection } = this; checkClickTime(function() { connection.file = !connection.file; @@ -139,6 +148,7 @@ class Body: Reactor.Component } event click $(icon.restart) { + if (!allow_perm_change_in_accept_window) return; var { cid, connection } = this; checkClickTime(function() { connection.restart = !connection.restart; @@ -148,6 +158,7 @@ class Body: Reactor.Component } event click $(icon.recording) { + if (!allow_perm_change_in_accept_window) return; var { cid, connection } = this; checkClickTime(function() { connection.recording = !connection.recording; @@ -157,6 +168,7 @@ class Body: Reactor.Component } event click $(icon.block_input) { + if (!allow_perm_change_in_accept_window) return; var { cid, connection } = this; checkClickTime(function() { connection.block_input = !connection.block_input; @@ -165,6 +177,16 @@ class Body: Reactor.Component }); } + event click $(icon.privacy_mode) { + if (!allow_perm_change_in_accept_window) return; + var { cid, connection } = this; + checkClickTime(function() { + connection.privacy_mode = !connection.privacy_mode; + body.update(); + handler.switch_permission(cid, "privacy_mode", connection.privacy_mode); + }); + } + event click $(button#accept) { var { cid, connection } = this; checkClickTime(function() { @@ -368,7 +390,7 @@ function bring_to_top(idx=-1) { } } -handler.addConnection = function(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, restart, recording, block_input) { +handler.addConnection = function(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, privacy_mode) { stdout.println("new connection #" + id + ": " + peer_id); var conn; connections.map(function(c) { @@ -376,6 +398,7 @@ handler.addConnection = function(id, is_file_transfer, is_view_camera, is_termin }); if (conn) { conn.authorized = authorized; + conn.privacy_mode = privacy_mode; update(); return; } @@ -391,7 +414,7 @@ handler.addConnection = function(id, is_file_transfer, is_view_camera, is_termin name: name, authorized: authorized, time: new Date(), now: new Date(), keyboard: keyboard, clipboard: clipboard, msgs: [], unreaded: 0, audio: audio, file: file, restart: restart, recording: recording, - block_input:block_input, + block_input:block_input, privacy_mode:privacy_mode, disconnected: false }; if (idx < 0) { @@ -480,15 +503,21 @@ function getElapsed(time, now) { return out; } -var ui_status_cache = [""]; +var ui_status_cache = ["", ""]; function check_update_ui() { self.timer(1s, function() { var approve_mode = handler.get_option('approve-mode'); + var allow_perm_change = handler.get_builtin_option('enable-perm-change-in-accept-window'); var changed = false; if (ui_status_cache[0] != approve_mode) { ui_status_cache[0] = approve_mode; changed = true; } + if (ui_status_cache[1] != allow_perm_change) { + ui_status_cache[1] = allow_perm_change; + allow_perm_change_in_accept_window = allow_perm_change != 'N'; + changed = true; + } if (changed) update(); check_update_ui(); }); diff --git a/src/ui/header.tis b/src/ui/header.tis index 2698ce4d0..40ccbcbf2 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -218,7 +218,7 @@ class Header: Reactor.Component { {is_file_copy_paste_supported && file_enabled ?
  • {svg_checkmark}{translate('Enable file copy and paste')}
  • : ""} {keyboard_enabled && clipboard_enabled ?
  • {svg_checkmark}{translate('Disable clipboard')}
  • : ""} {keyboard_enabled ?
  • {svg_checkmark}{translate('Lock after session end')}
  • : ""} - {keyboard_enabled && pi.platform == "Windows" ?
  • {svg_checkmark}{translate('Privacy mode')}
  • : ""} + {(pi.platform == "Windows" || pi.platform == "Mac OS") && (handler.get_toggle_option("privacy-mode") || (keyboard_enabled && privacy_mode_enabled)) ?
  • {svg_checkmark}{translate('Privacy mode')}
  • : ""} {keyboard_enabled && ((is_osx && pi.platform != "Mac OS") || (!is_osx && pi.platform == "Mac OS")) ?
  • {svg_checkmark}{translate('Swap control-command key')}
  • : ""} {handler.version_cmp(pi.version, '1.2.4') >= 0 ?
  • {svg_checkmark}{translate('True color (4:4:4)')}
  • : ""} diff --git a/src/ui/index.tis b/src/ui/index.tis index be826529d..a099b95f9 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -521,6 +521,7 @@ class MyIdMenu: Reactor.Component { {!disable_settings &&
  • {svg_checkmark}{translate('Enable remote restart')}
  • } {!disable_settings &&
  • {svg_checkmark}{translate('Enable TCP tunneling')}
  • } {!disable_settings && is_win ?
  • {svg_checkmark}{translate('Enable blocking user input')}
  • : ""} + {!disable_settings && (handler.get_supported_privacy_mode_impls() != '[]') &&
  • {svg_checkmark}{translate('Enable privacy mode')}
  • } {!disable_settings &&
  • {svg_checkmark}{translate('Enable LAN discovery')}
  • } diff --git a/src/ui/remote.tis b/src/ui/remote.tis index 7602432fe..28fbc3763 100644 --- a/src/ui/remote.tis +++ b/src/ui/remote.tis @@ -17,6 +17,7 @@ var audio_enabled = true; // server side var file_enabled = true; // server side var restart_enabled = true; // server side var recording_enabled = true; // server side +var privacy_mode_enabled = true; // server side var scroll_body = $(body); var peer_platform = ""; @@ -588,6 +589,7 @@ handler.setPermission = function(name, enabled) { if (name == "clipboard") clipboard_enabled = enabled; if (name == "restart") restart_enabled = enabled; if (name == "recording") recording_enabled = enabled; + if (name == "privacy_mode") privacy_mode_enabled = enabled; input_blocked = false; header.update(); }); diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 19a9e74e7..831824947 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -12,7 +12,10 @@ use hbb_common::fs::serialize_transfer_job; use hbb_common::tokio::sync::mpsc::unbounded_channel; use hbb_common::{ allow_err, bail, - config::{keys::OPTION_FILE_TRANSFER_MAX_FILES, Config}, + config::{ + keys::{OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW, OPTION_FILE_TRANSFER_MAX_FILES}, + option2bool, Config, + }, fs::{self, get_string, is_write_need_confirmation, new_send_confirm, DigestCheckResult}, log, message_proto::*, @@ -25,10 +28,7 @@ use hbb_common::{ ResultType, }; #[cfg(target_os = "windows")] -use hbb_common::{ - config::{keys::*, option2bool}, - tokio::sync::Mutex as TokioMutex, -}; +use hbb_common::{config::keys::*, tokio::sync::Mutex as TokioMutex}; use serde_derive::Serialize; #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] use std::iter::FromIterator; @@ -143,6 +143,7 @@ pub struct Client { pub restart: bool, pub recording: bool, pub block_input: bool, + pub privacy_mode: bool, pub from_switch: bool, pub in_voice_call: bool, pub incoming_voice_call: bool, @@ -230,6 +231,7 @@ impl ConnectionManager { restart: bool, recording: bool, block_input: bool, + privacy_mode: bool, from_switch: bool, #[cfg(not(any(target_os = "ios")))] tx: mpsc::UnboundedSender, ) { @@ -251,6 +253,7 @@ impl ConnectionManager { restart, recording, block_input, + privacy_mode, from_switch, #[cfg(not(any(target_os = "ios")))] tx, @@ -392,6 +395,23 @@ pub fn send_chat(id: i32, text: String) { #[inline] #[cfg(not(any(target_os = "ios")))] pub fn switch_permission(id: i32, name: String, enabled: bool) { + #[cfg(target_os = "android")] + let is_keyboard_permission = name == "keyboard"; + #[cfg(not(target_os = "android"))] + let is_keyboard_permission = false; + if !option2bool( + OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW, + &crate::get_builtin_option(OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW), + ) && !is_keyboard_permission + { + log::info!( + "blocked cm switch_permission by policy, conn_id={}, permission={}, enabled={}", + id, + name, + enabled + ); + return; + } if let Some(client) = CLIENTS.read().unwrap().get(&id) { allow_err!(client.tx.send(Data::SwitchPermission { name, enabled })); }; @@ -400,6 +420,19 @@ pub fn switch_permission(id: i32, name: String, enabled: bool) { #[inline] #[cfg(target_os = "android")] pub fn switch_permission_all(name: String, enabled: bool) { + if name != "keyboard" + && !option2bool( + OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW, + &crate::get_builtin_option(OPTION_ENABLE_PERM_CHANGE_IN_ACCEPT_WINDOW), + ) + { + log::info!( + "blocked cm switch_permission_all by policy, permission={}, enabled={}", + name, + enabled + ); + return; + } for (_, client) in CLIENTS.read().unwrap().iter() { allow_err!(client.tx.send(Data::SwitchPermission { name: name.clone(), @@ -422,6 +455,13 @@ pub fn get_clients_length() -> usize { clients.len() } +#[inline] +#[cfg(target_os = "android")] +pub fn has_active_clients() -> bool { + let clients = CLIENTS.read().unwrap(); + clients.values().any(|c| !c.disconnected) +} + #[inline] #[cfg(feature = "flutter")] #[cfg(not(any(target_os = "ios")))] @@ -503,9 +543,9 @@ impl IpcTaskRunner { } Ok(Some(data)) => { match data { - Data::Login{id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording, block_input, from_switch} => { + Data::Login{id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording, block_input, privacy_mode, from_switch} => { log::debug!("conn_id: {}", id); - self.cm.add_connection(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, from_switch, self.tx.clone()); + self.cm.add_connection(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, avatar, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, privacy_mode, from_switch, self.tx.clone()); self.conn_id = id; #[cfg(target_os = "windows")] { @@ -533,6 +573,26 @@ impl IpcTaskRunner { Data::ChatMessage { text } => { self.cm.new_message(self.conn_id, text); } + Data::SwitchPermission { name, enabled } => { + // Keep this branch scoped to privacy mode rollback. + // Other CM permission toggles are updated optimistically by the UI itself. + // The backend currently sends SwitchPermission back to CM only when + // privacy-mode turn-off fails and the UI state must be restored. + if name == "privacy_mode" { + let client = { + let mut clients = CLIENTS.write().unwrap(); + clients.get_mut(&self.conn_id).map(|c| { + c.privacy_mode = enabled; + c.clone() + }) + }; + if let Some(client) = client { + // This reuses add_connection(), and cm.tis only selectively updates + // existing rows (authorized/privacy_mode) for this fallback path. + self.cm.ui_handler.add_connection(&client); + } + } + } Data::FS(mut fs) => { if let ipc::FS::WriteBlock { id, file_num, data: _, compressed } = fs { if let Ok(bytes) = self.stream.next_raw().await { @@ -835,6 +895,7 @@ pub async fn start_listen( restart, recording, block_input, + privacy_mode, from_switch, .. }) => { @@ -856,6 +917,7 @@ pub async fn start_listen( restart, recording, block_input, + privacy_mode, from_switch, tx.clone(), ); From 253d632709b68f3b52464ba0661f6ce1ae47fd37 Mon Sep 17 00:00:00 2001 From: solokot Date: Mon, 4 May 2026 11:49:49 +0300 Subject: [PATCH 245/277] Update ru.rs (#14947) --- src/lang/ru.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 20000cd26..3917c6fa2 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -743,6 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Отображаемое имя"), ("password-hidden-tip", "Установлен постоянный пароль (скрытый)."), ("preset-password-in-use-tip", "Установленный пароль сейчас используется."), - ("Enable privacy mode", ""), + ("Enable privacy mode", "Использовать режим конфиденциальности"), ].iter().cloned().collect(); } From 52d62da00268d3a5f986b96a63f014370791a324 Mon Sep 17 00:00:00 2001 From: bilimiyorum <131397022+bilimiyorum@users.noreply.github.com> Date: Mon, 4 May 2026 11:50:23 +0300 Subject: [PATCH 246/277] Update tr.rs (#14948) 1- New string entry 2- A minor improvement for terminological consistency --- src/lang/tr.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 40eb561ed..d93ad4f68 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -741,8 +741,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Gelen oturumlar süresince ekranı açık tutun"), ("Continue with {}", "{} ile devam et"), ("Display Name", "Görünen Ad"), - ("password-hidden-tip", "Şifre gizli"), - ("preset-password-in-use-tip", "Önceden ayarlanmış şifre kullanılıyor"), - ("Enable privacy mode", ""), + ("password-hidden-tip", "Parola gizli"), + ("preset-password-in-use-tip", "Önceden ayarlanmış parola kullanılıyor"), + ("Enable privacy mode", "Gizlilik modunu etkinleştir"), ].iter().cloned().collect(); } From 5abae617dc8a5c6aea3f0c053832c1a89566d453 Mon Sep 17 00:00:00 2001 From: bovirus <1262554+bovirus@users.noreply.github.com> Date: Mon, 4 May 2026 10:50:42 +0200 Subject: [PATCH 247/277] Italian language update (#14949) --- src/lang/it.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/it.rs b/src/lang/it.rs index b83ee01ed..479551fcc 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -743,6 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Visualizza nome"), ("password-hidden-tip", "È impostata una password permanente (nascosta)."), ("preset-password-in-use-tip", "È attualmente in uso la password preimpostata."), - ("Enable privacy mode", ""), + ("Enable privacy mode", "Abilita modalità privacy"), ].iter().cloned().collect(); } From d5d0b01266edc8af6baabc2004a1096dd7088a02 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 29 Apr 2026 17:37:46 +0800 Subject: [PATCH 248/277] fix web break introduced in 38f130071 fix(linux): enable mouse side buttons in remote sessions (#14848) --- flutter/lib/common/widgets/toolbar.dart | 93 +++++++++++++++++-- flutter/lib/consts.dart | 2 + .../desktop/pages/desktop_setting_page.dart | 73 ++++++++++++++- flutter/lib/desktop/pages/remote_page.dart | 15 +++ .../lib/desktop/widgets/remote_toolbar.dart | 26 +++++- flutter/lib/mobile/pages/remote_page.dart | 13 +++ flutter/lib/mobile/pages/settings_page.dart | 19 ++++ flutter/lib/models/input_model.dart | 2 +- flutter/lib/models/model.dart | 8 ++ flutter/lib/web/bridge.dart | 25 +++++ 10 files changed, 266 insertions(+), 10 deletions(-) diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index 2e7247d95..da79c106e 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -16,16 +16,43 @@ import 'package:get/get.dart'; bool isEditOsPassword = false; +/// Action IDs that `toolbarControls` is the sole registrar for. Each call to +/// `toolbarControls` (e.g. opening the toolbar menu after a permission was +/// revoked or a state changed) wipes these so a previously-registered closure +/// can't outlive the menu entry that owns it. The for-loop at the bottom of +/// `toolbarControls` then re-registers whichever entries are still present in +/// the rebuilt menu list. +/// +/// Actions registered elsewhere — `registerSessionShortcutActions` on desktop +/// owns toggle_recording, fullscreen, switch_display, switch_tab, close_tab, +/// toggle_toolbar — MUST NOT appear here, otherwise this list would clobber +/// their registration on every menu rebuild. +/// +/// `kShortcutActionToggleRecording` is platform-conditional (mobile-only — +/// see the `!(isDesktop || isWeb)` guard in `toolbarControls`). It is handled +/// separately in the unregister pass rather than appearing in this const list. +const _kToolbarOwnedActionIds = [ + kShortcutActionSendCtrlAltDel, + kShortcutActionRestartRemote, + kShortcutActionInsertLock, + kShortcutActionToggleBlockInput, + kShortcutActionSwitchSides, + kShortcutActionRefresh, + kShortcutActionScreenshot, +]; + class TTextMenu { final Widget child; final VoidCallback? onPressed; Widget? trailingIcon; bool divider; + final String? actionId; TTextMenu( {required this.child, required this.onPressed, this.trailingIcon, - this.divider = false}); + this.divider = false, + this.actionId}); Widget getChild() { if (trailingIcon != null) { @@ -94,6 +121,20 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { final sessionId = ffi.sessionId; final isDefaultConn = ffi.connType == ConnType.defaultConn; + // Wipe everything `toolbarControls` could have registered last call so + // stale closures (e.g. for a menu entry whose permission has since been + // revoked) don't outlive the menu rebuild. See _kToolbarOwnedActionIds. + for (final actionId in _kToolbarOwnedActionIds) { + ffi.shortcutModel.unregister(actionId); + } + // toggle_recording is platform-conditional — toolbarControls only builds + // the menu entry on `!(isDesktop || isWeb)`. On desktop the registration + // is owned by `registerSessionShortcutActions` and must NOT be touched + // here. See the recording menu entry below. + if (!(isDesktop || isWeb)) { + ffi.shortcutModel.unregister(kShortcutActionToggleRecording); + } + List v = []; // elevation if (isDefaultConn && @@ -229,7 +270,8 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { v.add( TTextMenu( child: Text('${translate("Insert Ctrl + Alt + Del")}'), - onPressed: () => bind.sessionCtrlAltDel(sessionId: sessionId)), + onPressed: () => bind.sessionCtrlAltDel(sessionId: sessionId), + actionId: kShortcutActionSendCtrlAltDel), ); } // restart @@ -242,7 +284,8 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { TTextMenu( child: Text(translate('Restart remote device')), onPressed: () => - showRestartRemoteDevice(pi, id, sessionId, ffi.dialogManager)), + showRestartRemoteDevice(pi, id, sessionId, ffi.dialogManager), + actionId: kShortcutActionRestartRemote), ); } // insertLock @@ -250,7 +293,8 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { v.add( TTextMenu( child: Text(translate('Insert Lock')), - onPressed: () => bind.sessionLockScreen(sessionId: sessionId)), + onPressed: () => bind.sessionLockScreen(sessionId: sessionId), + actionId: kShortcutActionInsertLock), ); } // blockUserInput @@ -268,7 +312,8 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { sessionId: sessionId, value: '${blockInput.value ? 'un' : ''}block-input'); blockInput.value = !blockInput.value; - })); + }, + actionId: kShortcutActionToggleBlockInput)); } // switchSides if (isDefaultConn && @@ -280,13 +325,15 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { v.add(TTextMenu( child: Text(translate('Switch Sides')), onPressed: () => - showConfirmSwitchSidesDialog(sessionId, id, ffi.dialogManager))); + showConfirmSwitchSidesDialog(sessionId, id, ffi.dialogManager), + actionId: kShortcutActionSwitchSides)); } // refresh if (pi.version.isNotEmpty) { v.add(TTextMenu( child: Text(translate('Refresh')), onPressed: () => sessionRefreshVideo(sessionId, pi), + actionId: kShortcutActionRefresh, )); } // record @@ -308,7 +355,8 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { ) ], ), - onPressed: () => ffi.recordingModel.toggle())); + onPressed: () => ffi.recordingModel.toggle(), + actionId: kShortcutActionToggleRecording)); } // to-do: @@ -325,6 +373,14 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { onPressed: ffi.ffiModel.timerScreenshot != null ? null : () { + // Live cooldown check: the menu rebuilds onPressed=null + // whenever toolbarControls runs and finds timerScreenshot + // != null, but the keyboard-shortcut callback holds onto + // the originally-enabled closure across cooldown periods + // (toolbarControls only re-runs on menu open). Without + // this guard the second shortcut press during the 30s + // cooldown still fires sessionTakeScreenshot. + if (ffi.ffiModel.timerScreenshot != null) return; if (pi.currentDisplay == kAllDisplayValue) { msgBox( sessionId, @@ -342,6 +398,7 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { }); } }, + actionId: kShortcutActionScreenshot, )); } } @@ -352,6 +409,28 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { onPressed: () => onCopyFingerprint(FingerprintState.find(id).value), )); } + // Register tagged callbacks with the shortcut model so global keyboard + // shortcuts can dispatch the same actions as the toolbar menu items. + // + // For action IDs already cleared at the top of this function (i.e. those + // in [_kToolbarOwnedActionIds] plus the conditional toggle_recording), + // the `else` branch below is a redundant idempotent no-op — `unregister` + // just calls `Map.remove` on something already absent. + // + // The branch is kept as **defense in depth** for the case where a future + // contributor tags a menu item with an actionId that they forget to add + // to [_kToolbarOwnedActionIds]: without this `else`, the original + // "stale-closure-outlives-disabled-state" bug (e.g. Screenshot cooldown + // bypass) would silently come back for that new action only. + for (final menu in v) { + final actionId = menu.actionId; + if (actionId == null) continue; + if (menu.onPressed != null) { + ffi.shortcutModel.register(actionId, menu.onPressed!); + } else { + ffi.shortcutModel.unregister(actionId); + } + } return v; } diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 832b96d24..8362ed36e 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -4,6 +4,8 @@ import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:get/get.dart'; +export 'common/widgets/keyboard_shortcuts/shortcut_constants.dart'; + const int kMaxVirtualDisplayCount = 4; const int kAllVirtualDisplay = -1; diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 2841c1d27..b13b2c9cd 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -10,12 +10,14 @@ import 'package:flutter_hbb/common/widgets/audio_input.dart'; import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; +import 'package:flutter_hbb/desktop/pages/desktop_keyboard_shortcuts_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; import 'package:flutter_hbb/mobile/widgets/dialog.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/printer_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; +import 'package:flutter_hbb/models/shortcut_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/plugin/manager.dart'; import 'package:flutter_hbb/plugin/widgets/desktop_settings.dart'; @@ -421,11 +423,49 @@ class _GeneralState extends State<_General> { if (!isWeb) audio(context), if (!isWeb) record(context), if (!isWeb) WaylandCard(), - other() + other(), + if (!bind.isIncomingOnly()) keyboardShortcuts(), ], ).marginOnly(bottom: _kListViewBottomMargin); } + Widget keyboardShortcuts() { + // The bindings JSON (LocalConfig key `keyboard-shortcuts`) holds three + // flags + the bindings list: {enabled, pass_through, bindings}. When the + // master is off, the pass-through toggle and the Configure entry are + // hidden — both are meaningless without an active matcher. + return StatefulBuilder(builder: (context, setLocalState) { + final enabled = ShortcutModel.isEnabled(); + return _Card(title: 'Keyboard Shortcuts', children: [ + _OptionCheckBox( + context, + 'Enable keyboard shortcuts in remote session', + kShortcutLocalConfigKey, + isServer: false, + optGetter: ShortcutModel.isEnabled, + optSetter: (_, v) async { + await ShortcutModel.setEnabled(v); + setLocalState(() {}); + }, + ), + if (enabled) ...[ + _OptionCheckBox( + context, + 'Pass-through to remote', + kShortcutLocalConfigKey, + isServer: false, + optGetter: ShortcutModel.isPassThrough, + optSetter: (_, v) async { + await ShortcutModel.setPassThrough(v); + setLocalState(() {}); + }, + ), + _ShortcutsConfigureRow(), + ], + ]); + }); + } + Widget theme() { final current = MyTheme.getThemeModePreference().toShortString(); onChanged(String value) async { @@ -2950,6 +2990,37 @@ class _CountDownButtonState extends State<_CountDownButton> { } } +// Tappable row that pushes the shortcut configuration page. +class _ShortcutsConfigureRow extends StatelessWidget { + // ignore: unused_element + const _ShortcutsConfigureRow({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (_) => const DesktopKeyboardShortcutsPage(), + )); + }, + child: Row( + children: [ + Expanded( + child: Text(translate('Configure shortcuts...')), + ), + Icon(Icons.arrow_forward_ios, + size: 16, color: disabledTextColor(context, true)) + .marginOnly(right: 4), + ], + ).marginOnly( + left: _kCheckBoxLeftMargin, + top: 6, + bottom: 6, + ), + ); + } +} + //#endregion //#region dialogs diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 29e710bbc..944962573 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -17,6 +17,7 @@ import '../../common/widgets/toolbar.dart'; import '../../models/model.dart'; import '../../models/input_model.dart'; import '../../models/platform_model.dart'; +import '../../models/shortcut_model.dart'; import '../../common/shared_state.dart'; import '../../utils/image.dart'; import '../widgets/remote_toolbar.dart'; @@ -126,6 +127,20 @@ class _RemotePageState extends State _ffi.ffiModel.pi.platform, _ffi.dialogManager); _ffi.recordingModel .updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId)); + // Seed shortcut action callbacks once the session is ready, so that + // global keyboard shortcuts work even if the user never opens the + // toolbar menu. The returned list is intentionally discarded — the + // side effect of registering callbacks (inside toolbarControls) is + // what we want here. + if (mounted) { + toolbarControls(context, widget.id, _ffi); + // Register the default-bound actions that `toolbarControls` doesn't + // own (fullscreen, switch display, switch tab). Done in addition, + // not instead of, the toolbar registration above. + registerSessionShortcutActions(_ffi, + tabController: widget.tabController, + toolbarState: widget.toolbarState); + } }); _ffi.canvasModel.initializeEdgeScrollFallback(this); _ffi.start( diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 5da253e80..038c264aa 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common/widgets/audio_input.dart'; import 'package:flutter_hbb/common/widgets/dialog.dart'; +import 'package:flutter_hbb/common/widgets/keyboard_shortcuts/display.dart'; import 'package:flutter_hbb/common/widgets/toolbar.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; @@ -763,8 +764,31 @@ class _ControlMenu extends StatelessWidget { if (e.divider) { return Divider(); } else { + final hint = e.actionId == null + ? null + : ShortcutDisplay.formatFor(e.actionId!); + final child = hint == null + ? e.child + : Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible(child: e.child), + Padding( + padding: const EdgeInsets.only(left: 16), + child: Text( + hint, + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + color: Theme.of(context).hintColor, + ), + ), + ), + ], + ); return MenuButton( - child: e.child, + child: child, onPressed: e.onPressed, ffi: ffi, trailingIcon: e.trailingIcon); diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 74a5af45c..3a5256841 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -21,6 +21,7 @@ import '../../common/widgets/remote_input.dart'; import '../../models/input_model.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; +import '../../models/shortcut_model.dart'; import '../../utils/image.dart'; import '../widgets/dialog.dart'; import '../widgets/custom_scale_widget.dart'; @@ -119,6 +120,18 @@ class _RemotePageState extends State with WidgetsBindingObserver { } _disableAndroidSoftKeyboard( isKeyboardVisible: keyboardVisibilityController.isVisible); + // Seed shortcut action callbacks once the session is ready, so that + // global keyboard shortcuts work even if the user never opens the + // toolbar menu. The returned list is intentionally discarded — the + // side effect of registering callbacks (inside toolbarControls) is + // what we want here. + if (mounted) { + toolbarControls(context, widget.id, gFFI); + // Mobile has no DesktopTabController, so tab-switch shortcuts + // remain unregistered (they will simply log a no-handler debug + // line if a mobile user binds one — they have no tabs to switch). + registerSessionShortcutActions(gFFI); + } }); WidgetsBinding.instance.addObserver(this); } diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 509260636..ed766cf76 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -17,8 +17,10 @@ import '../../common/widgets/login.dart'; import '../../consts.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; +import '../../models/shortcut_model.dart'; import '../widgets/dialog.dart'; import 'home_page.dart'; +import 'mobile_keyboard_shortcuts_page.dart'; import 'scan_page.dart'; class SettingsPage extends StatefulWidget implements PageShape { @@ -819,6 +821,22 @@ class _SettingsState extends State with WidgetsBindingObserver { showThemeSettings(gFFI.dialogManager); }, ), + SettingsTile.navigation( + leading: Icon(Icons.keyboard_outlined), + title: Text(translate('Keyboard Shortcuts')), + description: Text(ShortcutModel.isEnabled() + ? translate('On') + : translate('Off')), + onPressed: (context) { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const MobileKeyboardShortcutsPage(), + )).then((_) { + if (mounted) setState(() {}); + }); + }, + ), if (!bind.isDisableAccount()) SettingsTile.switchTile( title: Text(translate('note-at-conn-end-tip')), @@ -1352,3 +1370,4 @@ SettingsTile _getPopupDialogRadioEntry({ ), ); } + diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 6fdffd796..984d6a25c 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -346,7 +346,7 @@ class InputModel { /// which runs per-engine, so each isolate registers its own handler tied /// to its own set of InputModels. static void initSideButtonChannel() { - if (!Platform.isLinux) return; + if (!isLinux) return; if (_sideButtonChannelInitialized) return; _sideButtonChannelInitialized = true; diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index e94834a2b..72ecdc99d 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -21,6 +21,7 @@ import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/printer_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; +import 'package:flutter_hbb/models/shortcut_model.dart'; import 'package:flutter_hbb/models/user_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/desktop_render_texture.dart'; @@ -476,6 +477,11 @@ class FfiModel with ChangeNotifier { } else if (name == 'exit_relative_mouse_mode') { // Handle exit shortcut from rdev grab loop (Ctrl+Alt on Win/Linux, Cmd+G on macOS) parent.target?.inputModel.exitRelativeMouseModeWithKeyRelease(); + } else if (name == kShortcutEventName) { + final action = evt['action']; + if (action is String) { + parent.target?.shortcutModel.onTriggered(action); + } } else { debugPrint('Event is not handled in the fixed branch: $name'); } @@ -3623,6 +3629,7 @@ class FFI { late final ElevationModel elevationModel; // session late final CmFileModel cmFileModel; // cm late final TextureModel textureModel; //session + late final ShortcutModel shortcutModel; // session late final Peers recentPeersModel; // global late final Peers favoritePeersModel; // global late final Peers lanPeersModel; // global @@ -3652,6 +3659,7 @@ class FFI { elevationModel = ElevationModel(WeakReference(this)); cmFileModel = CmFileModel(WeakReference(this)); textureModel = TextureModel(WeakReference(this)); + shortcutModel = ShortcutModel(WeakReference(this)); recentPeersModel = Peers( name: PeersModelName.recent, loadEvent: LoadEvent.recent, diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index 54e6a9a9b..f151a6e46 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -7,6 +7,7 @@ import 'package:uuid/uuid.dart'; import 'dart:html' as html; import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/common.dart' as common; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); @@ -930,6 +931,21 @@ class RustdeskImpl { ])); } + // Tell the JS-side matcher (flutter/web/js/src/shortcut_matcher.ts) to + // re-read its bindings from LocalStorage. Mirrors the native call which + // refreshes the Rust matcher's in-memory cache. + void mainReloadKeyboardShortcuts({dynamic hint}) { + js.context.callMethod('reloadShortcuts', []); + } + + // Web has no Rust at runtime, so the defaults seed comes from the + // [kDefaultShortcutBindings] canonical in shortcut_constants.dart. Parity + // with Rust's `default_bindings()` is enforced by tests on both sides + // against `flutter/test/fixtures/default_keyboard_shortcuts.json`. + String mainGetDefaultKeyboardShortcuts({dynamic hint}) { + return jsonEncode(kDefaultShortcutBindings); + } + String mainGetInputSource({dynamic hint}) { final inputSource = js.context.callMethod('getByName', ['option:local', 'input-source']); @@ -1176,6 +1192,15 @@ class RustdeskImpl { } Future mainInit({required String appDir, dynamic hint}) { + // JS -> Dart shortcut bridge. The matcher in flutter/web/js/src/ + // shortcut_matcher.ts calls `window.onShortcutTriggered(actionId)` when a + // binding fires; route it to the active session's ShortcutModel. + // Web is single-window so `gFFI` is always the active session. + js.context['onShortcutTriggered'] = (dynamic action) { + if (action is String) { + common.gFFI.shortcutModel.onTriggered(action); + } + }; return Future.value(); } From f29dec7b13c25e2d7f1c5db4a2310522a2112836 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 6 May 2026 19:27:56 +0800 Subject: [PATCH 249/277] harden switch side --- libs/hbb_common | 2 +- src/client.rs | 77 ++++++++++++++++++++++++++++++++++--- src/client/io_loop.rs | 18 ++++++++- src/flutter_ffi.rs | 2 +- src/ipc.rs | 22 +++++++++++ src/server/connection.rs | 39 ++++++++++++++++++- src/ui_cm_interface.rs | 2 +- src/ui_session_interface.rs | 5 ++- 8 files changed, 153 insertions(+), 14 deletions(-) diff --git a/libs/hbb_common b/libs/hbb_common index 3e31a9493..87b11a795 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 3e31a94939e026ab2c05d21a2c436960aa9bfea8 +Subproject commit 87b11a795964b00deded250657a63626f2c1efa0 diff --git a/src/client.rs b/src/client.rs index 72652776a..321a49ee6 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1745,6 +1745,9 @@ pub struct LoginConfigHandler { pub direct: Option, pub received: bool, switch_uuid: Option, + #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + switch_back_allowed: bool, pub save_ab_password_to_recent: bool, // true: connected with ab password pub other_server: Option<(String, String, String)>, pub custom_fps: Arc>>, @@ -1861,6 +1864,11 @@ impl LoginConfigHandler { self.direct = None; self.received = false; + #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + self.switch_back_allowed = false; + } self.switch_uuid = switch_uuid; self.adapter_luid = adapter_luid; self.selected_windows_session_id = None; @@ -1874,6 +1882,23 @@ impl LoginConfigHandler { self.is_terminal_admin = is_terminal_admin; } + #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + pub fn allow_switch_back_once(&mut self) { + self.switch_back_allowed = true; + } + + #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + pub fn consume_switch_back_permission(&mut self) -> bool { + if self.switch_back_allowed { + self.switch_back_allowed = false; + true + } else { + false + } + } + /// Check if the client should auto login. /// Return password if the client should auto login, otherwise return empty string. pub fn should_auto_login(&self) -> String { @@ -3377,6 +3402,36 @@ pub fn handle_login_error( } } +#[cfg(feature = "flutter")] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +async fn consume_local_switch_sides_uuid(id: &str, uuid: &Uuid) -> bool { + let Ok(mut conn) = crate::ipc::connect(1000, "").await else { + return false; + }; + let uuid = uuid.to_string(); + if conn + .send(&crate::ipc::Data::SwitchSidesUuid( + uuid.clone(), + id.to_owned(), + None, + )) + .await + .is_err() + { + return false; + } + match conn.next_timeout(1000).await { + Ok(Some(crate::ipc::Data::SwitchSidesUuid( + returned_uuid, + returned_id, + Some(true), + ))) => { + returned_uuid == uuid && returned_id == id + } + _ => false, + } +} + /// Handle hash message sent by peer. /// Hash will be used for login. /// @@ -3397,12 +3452,22 @@ pub async fn handle_hash( // Take care of password application order // switch_uuid - let uuid = lc.write().unwrap().switch_uuid.take(); - if let Some(uuid) = uuid { - if let Ok(uuid) = uuid::Uuid::from_str(&uuid) { - send_switch_login_request(lc.clone(), peer, uuid).await; - lc.write().unwrap().password_source = Default::default(); - return; + #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + let uuid = lc.write().unwrap().switch_uuid.take(); + if let Some(uuid) = uuid { + if let Ok(uuid) = uuid::Uuid::from_str(&uuid) { + let id = lc.read().unwrap().id.clone(); + if !consume_local_switch_sides_uuid(&id, &uuid).await { + log::warn!("Ignored untrusted switch_uuid"); + } else { + lc.write().unwrap().allow_switch_back_once(); + send_switch_login_request(lc.clone(), peer, uuid).await; + lc.write().unwrap().password_source = Default::default(); + return; + } + } } } // last password diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 78ba9ebc6..5eb7a273a 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1923,9 +1923,23 @@ impl Remote { ); } } + #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] Some(misc::Union::SwitchBack(_)) => { - #[cfg(feature = "flutter")] - self.handler.switch_back(&self.handler.get_id()); + let allow_switch_back = self + .handler + .lc + .write() + .unwrap() + .consume_switch_back_permission(); + if allow_switch_back { + self.handler.switch_back(&self.handler.get_id()); + } else { + log::warn!( + "Ignored unsolicited SwitchBack from {}", + self.handler.get_id() + ); + } } #[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 3f97df078..4b62b4fca 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -2213,7 +2213,7 @@ pub fn cm_elevate_portable(conn_id: i32) { } pub fn cm_switch_back(conn_id: i32) { - #[cfg(not(any(target_os = "ios")))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] crate::ui_cm_interface::switch_back(conn_id); } diff --git a/src/ipc.rs b/src/ipc.rs index e6d4fc834..82b52a60c 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -285,7 +285,14 @@ pub enum Data { Empty, Disconnected, DataPortableService(DataPortableService), + #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] SwitchSidesRequest(String), + #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + SwitchSidesUuid(String, String, Option), + #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] SwitchSidesBack, UrlLink(String), VoiceCallIncoming, @@ -771,6 +778,8 @@ async fn handle(data: Data, stream: &mut Connection) { Data::TestRendezvousServer => { crate::test_rendezvous_server(); } + #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] Data::SwitchSidesRequest(id) => { let uuid = uuid::Uuid::new_v4(); crate::server::insert_switch_sides_uuid(id, uuid.clone()); @@ -780,6 +789,19 @@ async fn handle(data: Data, stream: &mut Connection) { .await ); } + #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Data::SwitchSidesUuid(uuid, id, None) => { + let allowed = uuid + .parse::() + .map(|uuid| crate::server::remove_pending_switch_sides_uuid(&id, &uuid)) + .unwrap_or(false); + allow_err!( + stream + .send(&Data::SwitchSidesUuid(uuid, id, Some(allowed))) + .await + ); + } #[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] Data::Plugin(plugin) => crate::plugin::ipc::handle_plugin(plugin, stream).await, diff --git a/src/server/connection.rs b/src/server/connection.rs index bd5327bb2..a960daac1 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -73,11 +73,17 @@ lazy_static::lazy_static! { static ref ALIVE_CONNS: Arc::>> = Default::default(); pub static ref AUTHED_CONNS: Arc::>> = Default::default(); pub static ref CONTROL_PERMISSIONS_ARRAY: Arc::>> = Default::default(); - static ref SWITCH_SIDES_UUID: Arc::>> = Default::default(); static ref WAKELOCK_SENDER: Arc::>> = Arc::new(Mutex::new(start_wakelock_thread())); static ref WAKELOCK_KEEP_AWAKE_OPTION: Arc::>> = Default::default(); } +#[cfg(feature = "flutter")] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +lazy_static::lazy_static! { + static ref SWITCH_SIDES_UUID: Arc::>> = Default::default(); + static ref PENDING_SWITCH_SIDES_UUID: Arc::>> = Default::default(); +} + fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { if a.len() != b.len() { return false; @@ -775,6 +781,8 @@ impl Connection { log::error!("Failed to start portable service from cm: {:?}", e); } } + #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] ipc::Data::SwitchSidesBack => { let mut misc = Misc::new(); misc.set_switch_back(SwitchBack::default()); @@ -2579,6 +2587,7 @@ impl Connection { } } else if let Some(message::Union::SwitchSidesResponse(_s)) = msg.union { #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] if let Some(lr) = _s.lr.clone().take() { self.handle_login_request_without_validation(&lr).await; SWITCH_SIDES_UUID @@ -3294,8 +3303,13 @@ impl Connection { } } #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] Some(misc::Union::SwitchSidesRequest(s)) => { if let Ok(uuid) = uuid::Uuid::from_slice(&s.uuid.to_vec()[..]) { + crate::server::insert_pending_switch_sides_uuid( + self.lr.my_id.clone(), + uuid.clone(), + ); crate::run_me(vec![ "--connect", &self.lr.my_id, @@ -4938,6 +4952,8 @@ impl Connection { } } +#[cfg(feature = "flutter")] +#[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { SWITCH_SIDES_UUID .lock() @@ -4945,6 +4961,27 @@ pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { .insert(id, (tokio::time::Instant::now(), uuid)); } +#[cfg(feature = "flutter")] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn insert_pending_switch_sides_uuid(id: String, uuid: uuid::Uuid) { + let mut uuids = PENDING_SWITCH_SIDES_UUID.lock().unwrap(); + uuids.retain(|_, (instant, _)| instant.elapsed() < Duration::from_secs(10)); + uuids.insert(id, (tokio::time::Instant::now(), uuid)); +} + +#[cfg(feature = "flutter")] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn remove_pending_switch_sides_uuid(id: &str, uuid: &uuid::Uuid) -> bool { + let mut uuids = PENDING_SWITCH_SIDES_UUID.lock().unwrap(); + uuids.retain(|_, (instant, _)| instant.elapsed() < Duration::from_secs(10)); + if uuids.get(id).map(|(_, stored_uuid)| stored_uuid == uuid) == Some(true) { + uuids.remove(id); + true + } else { + false + } +} + #[cfg(not(any(target_os = "android", target_os = "ios")))] async fn start_ipc( mut rx_to_cm: mpsc::UnboundedReceiver, diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 831824947..cab0d7f1c 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -464,7 +464,7 @@ pub fn has_active_clients() -> bool { #[inline] #[cfg(feature = "flutter")] -#[cfg(not(any(target_os = "ios")))] +#[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn switch_back(id: i32) { if let Some(client) = CLIENTS.read().unwrap().get(&id) { allow_err!(client.tx.send(Data::SwitchSidesBack)); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index c18c17fe2..e6c8ac6a2 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1464,10 +1464,11 @@ impl Session { self.send(Data::ElevateWithLogon(username, password)); } - #[cfg(any(target_os = "ios"))] + #[cfg(any(target_os = "android", target_os = "ios", not(feature = "flutter")))] pub fn switch_sides(&self) {} - #[cfg(not(any(target_os = "ios")))] + #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] #[tokio::main(flavor = "current_thread")] pub async fn switch_sides(&self) { match crate::ipc::connect(1000, "").await { From 9d1f86fbc6f5abdab7af6133abaf56003b9ad82f Mon Sep 17 00:00:00 2001 From: Mr-Update <37781396+Mr-Update@users.noreply.github.com> Date: Wed, 6 May 2026 13:32:41 +0200 Subject: [PATCH 250/277] Update de.rs (#14953) --- src/lang/de.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/de.rs b/src/lang/de.rs index 7d18cd7a1..030bc626d 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -743,6 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Anzeigename"), ("password-hidden-tip", "Ein permanentes Passwort wurde festgelegt (ausgeblendet)."), ("preset-password-in-use-tip", "Das voreingestellte Passwort wird derzeit verwendet."), - ("Enable privacy mode", ""), + ("Enable privacy mode", "Datenschutzmodus aktivieren"), ].iter().cloned().collect(); } From 0221634a4da93c0f35a491d0ae55cbd284538d17 Mon Sep 17 00:00:00 2001 From: Lynilia <89228568+Lynilia@users.noreply.github.com> Date: Wed, 6 May 2026 13:32:59 +0200 Subject: [PATCH 251/277] Update fr.rs (#14955) --- src/lang/fr.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/fr.rs b/src/lang/fr.rs index ab6ed2e76..6f7bb2880 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -743,6 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Nom d’affichage"), ("password-hidden-tip", "Le mot de passe permanent est défini (masqué)."), ("preset-password-in-use-tip", "Le mot de passe prédéfini est actuellement utilisé."), - ("Enable privacy mode", ""), + ("Enable privacy mode", "Activer le mode de confidentialité"), ].iter().cloned().collect(); } From 92509f8e8a17f07d881c4f566fc3ad6cddb3e074 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 6 May 2026 19:35:13 +0800 Subject: [PATCH 252/277] update hbb_common --- libs/hbb_common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/hbb_common b/libs/hbb_common index 87b11a795..6490a8655 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 87b11a795964b00deded250657a63626f2c1efa0 +Subproject commit 6490a8655c25801e16c3b30d161d9f2b9e458b36 From 8b8a64f870c5126cef9deb9cf168ca3a6fa1e9e4 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 6 May 2026 19:40:52 +0800 Subject: [PATCH 253/277] revert hbb_common to old one --- libs/hbb_common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/hbb_common b/libs/hbb_common index 6490a8655..3e31a9493 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 6490a8655c25801e16c3b30d161d9f2b9e458b36 +Subproject commit 3e31a94939e026ab2c05d21a2c436960aa9bfea8 From 5439ec38b663c2ff9de1063ac125f6ac61d78ae2 Mon Sep 17 00:00:00 2001 From: 21pages Date: Wed, 6 May 2026 20:20:17 +0800 Subject: [PATCH 254/277] Revert "fix web break introduced in 38f130071 fix(linux): enable mouse side buttons in remote sessions (#14848)" (#14973) This reverts commit d5d0b01266edc8af6baabc2004a1096dd7088a02. --- flutter/lib/common/widgets/toolbar.dart | 93 ++----------------- flutter/lib/consts.dart | 2 - .../desktop/pages/desktop_setting_page.dart | 73 +-------------- flutter/lib/desktop/pages/remote_page.dart | 15 --- .../lib/desktop/widgets/remote_toolbar.dart | 26 +----- flutter/lib/mobile/pages/remote_page.dart | 13 --- flutter/lib/mobile/pages/settings_page.dart | 19 ---- flutter/lib/models/input_model.dart | 2 +- flutter/lib/models/model.dart | 8 -- flutter/lib/web/bridge.dart | 25 ----- 10 files changed, 10 insertions(+), 266 deletions(-) diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index da79c106e..2e7247d95 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -16,43 +16,16 @@ import 'package:get/get.dart'; bool isEditOsPassword = false; -/// Action IDs that `toolbarControls` is the sole registrar for. Each call to -/// `toolbarControls` (e.g. opening the toolbar menu after a permission was -/// revoked or a state changed) wipes these so a previously-registered closure -/// can't outlive the menu entry that owns it. The for-loop at the bottom of -/// `toolbarControls` then re-registers whichever entries are still present in -/// the rebuilt menu list. -/// -/// Actions registered elsewhere — `registerSessionShortcutActions` on desktop -/// owns toggle_recording, fullscreen, switch_display, switch_tab, close_tab, -/// toggle_toolbar — MUST NOT appear here, otherwise this list would clobber -/// their registration on every menu rebuild. -/// -/// `kShortcutActionToggleRecording` is platform-conditional (mobile-only — -/// see the `!(isDesktop || isWeb)` guard in `toolbarControls`). It is handled -/// separately in the unregister pass rather than appearing in this const list. -const _kToolbarOwnedActionIds = [ - kShortcutActionSendCtrlAltDel, - kShortcutActionRestartRemote, - kShortcutActionInsertLock, - kShortcutActionToggleBlockInput, - kShortcutActionSwitchSides, - kShortcutActionRefresh, - kShortcutActionScreenshot, -]; - class TTextMenu { final Widget child; final VoidCallback? onPressed; Widget? trailingIcon; bool divider; - final String? actionId; TTextMenu( {required this.child, required this.onPressed, this.trailingIcon, - this.divider = false, - this.actionId}); + this.divider = false}); Widget getChild() { if (trailingIcon != null) { @@ -121,20 +94,6 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { final sessionId = ffi.sessionId; final isDefaultConn = ffi.connType == ConnType.defaultConn; - // Wipe everything `toolbarControls` could have registered last call so - // stale closures (e.g. for a menu entry whose permission has since been - // revoked) don't outlive the menu rebuild. See _kToolbarOwnedActionIds. - for (final actionId in _kToolbarOwnedActionIds) { - ffi.shortcutModel.unregister(actionId); - } - // toggle_recording is platform-conditional — toolbarControls only builds - // the menu entry on `!(isDesktop || isWeb)`. On desktop the registration - // is owned by `registerSessionShortcutActions` and must NOT be touched - // here. See the recording menu entry below. - if (!(isDesktop || isWeb)) { - ffi.shortcutModel.unregister(kShortcutActionToggleRecording); - } - List v = []; // elevation if (isDefaultConn && @@ -270,8 +229,7 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { v.add( TTextMenu( child: Text('${translate("Insert Ctrl + Alt + Del")}'), - onPressed: () => bind.sessionCtrlAltDel(sessionId: sessionId), - actionId: kShortcutActionSendCtrlAltDel), + onPressed: () => bind.sessionCtrlAltDel(sessionId: sessionId)), ); } // restart @@ -284,8 +242,7 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { TTextMenu( child: Text(translate('Restart remote device')), onPressed: () => - showRestartRemoteDevice(pi, id, sessionId, ffi.dialogManager), - actionId: kShortcutActionRestartRemote), + showRestartRemoteDevice(pi, id, sessionId, ffi.dialogManager)), ); } // insertLock @@ -293,8 +250,7 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { v.add( TTextMenu( child: Text(translate('Insert Lock')), - onPressed: () => bind.sessionLockScreen(sessionId: sessionId), - actionId: kShortcutActionInsertLock), + onPressed: () => bind.sessionLockScreen(sessionId: sessionId)), ); } // blockUserInput @@ -312,8 +268,7 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { sessionId: sessionId, value: '${blockInput.value ? 'un' : ''}block-input'); blockInput.value = !blockInput.value; - }, - actionId: kShortcutActionToggleBlockInput)); + })); } // switchSides if (isDefaultConn && @@ -325,15 +280,13 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { v.add(TTextMenu( child: Text(translate('Switch Sides')), onPressed: () => - showConfirmSwitchSidesDialog(sessionId, id, ffi.dialogManager), - actionId: kShortcutActionSwitchSides)); + showConfirmSwitchSidesDialog(sessionId, id, ffi.dialogManager))); } // refresh if (pi.version.isNotEmpty) { v.add(TTextMenu( child: Text(translate('Refresh')), onPressed: () => sessionRefreshVideo(sessionId, pi), - actionId: kShortcutActionRefresh, )); } // record @@ -355,8 +308,7 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { ) ], ), - onPressed: () => ffi.recordingModel.toggle(), - actionId: kShortcutActionToggleRecording)); + onPressed: () => ffi.recordingModel.toggle())); } // to-do: @@ -373,14 +325,6 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { onPressed: ffi.ffiModel.timerScreenshot != null ? null : () { - // Live cooldown check: the menu rebuilds onPressed=null - // whenever toolbarControls runs and finds timerScreenshot - // != null, but the keyboard-shortcut callback holds onto - // the originally-enabled closure across cooldown periods - // (toolbarControls only re-runs on menu open). Without - // this guard the second shortcut press during the 30s - // cooldown still fires sessionTakeScreenshot. - if (ffi.ffiModel.timerScreenshot != null) return; if (pi.currentDisplay == kAllDisplayValue) { msgBox( sessionId, @@ -398,7 +342,6 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { }); } }, - actionId: kShortcutActionScreenshot, )); } } @@ -409,28 +352,6 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { onPressed: () => onCopyFingerprint(FingerprintState.find(id).value), )); } - // Register tagged callbacks with the shortcut model so global keyboard - // shortcuts can dispatch the same actions as the toolbar menu items. - // - // For action IDs already cleared at the top of this function (i.e. those - // in [_kToolbarOwnedActionIds] plus the conditional toggle_recording), - // the `else` branch below is a redundant idempotent no-op — `unregister` - // just calls `Map.remove` on something already absent. - // - // The branch is kept as **defense in depth** for the case where a future - // contributor tags a menu item with an actionId that they forget to add - // to [_kToolbarOwnedActionIds]: without this `else`, the original - // "stale-closure-outlives-disabled-state" bug (e.g. Screenshot cooldown - // bypass) would silently come back for that new action only. - for (final menu in v) { - final actionId = menu.actionId; - if (actionId == null) continue; - if (menu.onPressed != null) { - ffi.shortcutModel.register(actionId, menu.onPressed!); - } else { - ffi.shortcutModel.unregister(actionId); - } - } return v; } diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 8362ed36e..832b96d24 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -4,8 +4,6 @@ import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:get/get.dart'; -export 'common/widgets/keyboard_shortcuts/shortcut_constants.dart'; - const int kMaxVirtualDisplayCount = 4; const int kAllVirtualDisplay = -1; diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index b13b2c9cd..2841c1d27 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -10,14 +10,12 @@ import 'package:flutter_hbb/common/widgets/audio_input.dart'; import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; -import 'package:flutter_hbb/desktop/pages/desktop_keyboard_shortcuts_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; import 'package:flutter_hbb/mobile/widgets/dialog.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/printer_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; -import 'package:flutter_hbb/models/shortcut_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/plugin/manager.dart'; import 'package:flutter_hbb/plugin/widgets/desktop_settings.dart'; @@ -423,49 +421,11 @@ class _GeneralState extends State<_General> { if (!isWeb) audio(context), if (!isWeb) record(context), if (!isWeb) WaylandCard(), - other(), - if (!bind.isIncomingOnly()) keyboardShortcuts(), + other() ], ).marginOnly(bottom: _kListViewBottomMargin); } - Widget keyboardShortcuts() { - // The bindings JSON (LocalConfig key `keyboard-shortcuts`) holds three - // flags + the bindings list: {enabled, pass_through, bindings}. When the - // master is off, the pass-through toggle and the Configure entry are - // hidden — both are meaningless without an active matcher. - return StatefulBuilder(builder: (context, setLocalState) { - final enabled = ShortcutModel.isEnabled(); - return _Card(title: 'Keyboard Shortcuts', children: [ - _OptionCheckBox( - context, - 'Enable keyboard shortcuts in remote session', - kShortcutLocalConfigKey, - isServer: false, - optGetter: ShortcutModel.isEnabled, - optSetter: (_, v) async { - await ShortcutModel.setEnabled(v); - setLocalState(() {}); - }, - ), - if (enabled) ...[ - _OptionCheckBox( - context, - 'Pass-through to remote', - kShortcutLocalConfigKey, - isServer: false, - optGetter: ShortcutModel.isPassThrough, - optSetter: (_, v) async { - await ShortcutModel.setPassThrough(v); - setLocalState(() {}); - }, - ), - _ShortcutsConfigureRow(), - ], - ]); - }); - } - Widget theme() { final current = MyTheme.getThemeModePreference().toShortString(); onChanged(String value) async { @@ -2990,37 +2950,6 @@ class _CountDownButtonState extends State<_CountDownButton> { } } -// Tappable row that pushes the shortcut configuration page. -class _ShortcutsConfigureRow extends StatelessWidget { - // ignore: unused_element - const _ShortcutsConfigureRow({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return InkWell( - onTap: () { - Navigator.of(context).push(MaterialPageRoute( - builder: (_) => const DesktopKeyboardShortcutsPage(), - )); - }, - child: Row( - children: [ - Expanded( - child: Text(translate('Configure shortcuts...')), - ), - Icon(Icons.arrow_forward_ios, - size: 16, color: disabledTextColor(context, true)) - .marginOnly(right: 4), - ], - ).marginOnly( - left: _kCheckBoxLeftMargin, - top: 6, - bottom: 6, - ), - ); - } -} - //#endregion //#region dialogs diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 944962573..29e710bbc 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -17,7 +17,6 @@ import '../../common/widgets/toolbar.dart'; import '../../models/model.dart'; import '../../models/input_model.dart'; import '../../models/platform_model.dart'; -import '../../models/shortcut_model.dart'; import '../../common/shared_state.dart'; import '../../utils/image.dart'; import '../widgets/remote_toolbar.dart'; @@ -127,20 +126,6 @@ class _RemotePageState extends State _ffi.ffiModel.pi.platform, _ffi.dialogManager); _ffi.recordingModel .updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId)); - // Seed shortcut action callbacks once the session is ready, so that - // global keyboard shortcuts work even if the user never opens the - // toolbar menu. The returned list is intentionally discarded — the - // side effect of registering callbacks (inside toolbarControls) is - // what we want here. - if (mounted) { - toolbarControls(context, widget.id, _ffi); - // Register the default-bound actions that `toolbarControls` doesn't - // own (fullscreen, switch display, switch tab). Done in addition, - // not instead of, the toolbar registration above. - registerSessionShortcutActions(_ffi, - tabController: widget.tabController, - toolbarState: widget.toolbarState); - } }); _ffi.canvasModel.initializeEdgeScrollFallback(this); _ffi.start( diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 038c264aa..5da253e80 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common/widgets/audio_input.dart'; import 'package:flutter_hbb/common/widgets/dialog.dart'; -import 'package:flutter_hbb/common/widgets/keyboard_shortcuts/display.dart'; import 'package:flutter_hbb/common/widgets/toolbar.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; @@ -764,31 +763,8 @@ class _ControlMenu extends StatelessWidget { if (e.divider) { return Divider(); } else { - final hint = e.actionId == null - ? null - : ShortcutDisplay.formatFor(e.actionId!); - final child = hint == null - ? e.child - : Row( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible(child: e.child), - Padding( - padding: const EdgeInsets.only(left: 16), - child: Text( - hint, - style: Theme.of(context) - .textTheme - .bodySmall - ?.copyWith( - color: Theme.of(context).hintColor, - ), - ), - ), - ], - ); return MenuButton( - child: child, + child: e.child, onPressed: e.onPressed, ffi: ffi, trailingIcon: e.trailingIcon); diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 3a5256841..74a5af45c 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -21,7 +21,6 @@ import '../../common/widgets/remote_input.dart'; import '../../models/input_model.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; -import '../../models/shortcut_model.dart'; import '../../utils/image.dart'; import '../widgets/dialog.dart'; import '../widgets/custom_scale_widget.dart'; @@ -120,18 +119,6 @@ class _RemotePageState extends State with WidgetsBindingObserver { } _disableAndroidSoftKeyboard( isKeyboardVisible: keyboardVisibilityController.isVisible); - // Seed shortcut action callbacks once the session is ready, so that - // global keyboard shortcuts work even if the user never opens the - // toolbar menu. The returned list is intentionally discarded — the - // side effect of registering callbacks (inside toolbarControls) is - // what we want here. - if (mounted) { - toolbarControls(context, widget.id, gFFI); - // Mobile has no DesktopTabController, so tab-switch shortcuts - // remain unregistered (they will simply log a no-handler debug - // line if a mobile user binds one — they have no tabs to switch). - registerSessionShortcutActions(gFFI); - } }); WidgetsBinding.instance.addObserver(this); } diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index ed766cf76..509260636 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -17,10 +17,8 @@ import '../../common/widgets/login.dart'; import '../../consts.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; -import '../../models/shortcut_model.dart'; import '../widgets/dialog.dart'; import 'home_page.dart'; -import 'mobile_keyboard_shortcuts_page.dart'; import 'scan_page.dart'; class SettingsPage extends StatefulWidget implements PageShape { @@ -821,22 +819,6 @@ class _SettingsState extends State with WidgetsBindingObserver { showThemeSettings(gFFI.dialogManager); }, ), - SettingsTile.navigation( - leading: Icon(Icons.keyboard_outlined), - title: Text(translate('Keyboard Shortcuts')), - description: Text(ShortcutModel.isEnabled() - ? translate('On') - : translate('Off')), - onPressed: (context) { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => const MobileKeyboardShortcutsPage(), - )).then((_) { - if (mounted) setState(() {}); - }); - }, - ), if (!bind.isDisableAccount()) SettingsTile.switchTile( title: Text(translate('note-at-conn-end-tip')), @@ -1370,4 +1352,3 @@ SettingsTile _getPopupDialogRadioEntry({ ), ); } - diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 984d6a25c..6fdffd796 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -346,7 +346,7 @@ class InputModel { /// which runs per-engine, so each isolate registers its own handler tied /// to its own set of InputModels. static void initSideButtonChannel() { - if (!isLinux) return; + if (!Platform.isLinux) return; if (_sideButtonChannelInitialized) return; _sideButtonChannelInitialized = true; diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 72ecdc99d..e94834a2b 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -21,7 +21,6 @@ import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/printer_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; -import 'package:flutter_hbb/models/shortcut_model.dart'; import 'package:flutter_hbb/models/user_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/desktop_render_texture.dart'; @@ -477,11 +476,6 @@ class FfiModel with ChangeNotifier { } else if (name == 'exit_relative_mouse_mode') { // Handle exit shortcut from rdev grab loop (Ctrl+Alt on Win/Linux, Cmd+G on macOS) parent.target?.inputModel.exitRelativeMouseModeWithKeyRelease(); - } else if (name == kShortcutEventName) { - final action = evt['action']; - if (action is String) { - parent.target?.shortcutModel.onTriggered(action); - } } else { debugPrint('Event is not handled in the fixed branch: $name'); } @@ -3629,7 +3623,6 @@ class FFI { late final ElevationModel elevationModel; // session late final CmFileModel cmFileModel; // cm late final TextureModel textureModel; //session - late final ShortcutModel shortcutModel; // session late final Peers recentPeersModel; // global late final Peers favoritePeersModel; // global late final Peers lanPeersModel; // global @@ -3659,7 +3652,6 @@ class FFI { elevationModel = ElevationModel(WeakReference(this)); cmFileModel = CmFileModel(WeakReference(this)); textureModel = TextureModel(WeakReference(this)); - shortcutModel = ShortcutModel(WeakReference(this)); recentPeersModel = Peers( name: PeersModelName.recent, loadEvent: LoadEvent.recent, diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index f151a6e46..54e6a9a9b 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -7,7 +7,6 @@ import 'package:uuid/uuid.dart'; import 'dart:html' as html; import 'package:flutter_hbb/consts.dart'; -import 'package:flutter_hbb/common.dart' as common; final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); @@ -931,21 +930,6 @@ class RustdeskImpl { ])); } - // Tell the JS-side matcher (flutter/web/js/src/shortcut_matcher.ts) to - // re-read its bindings from LocalStorage. Mirrors the native call which - // refreshes the Rust matcher's in-memory cache. - void mainReloadKeyboardShortcuts({dynamic hint}) { - js.context.callMethod('reloadShortcuts', []); - } - - // Web has no Rust at runtime, so the defaults seed comes from the - // [kDefaultShortcutBindings] canonical in shortcut_constants.dart. Parity - // with Rust's `default_bindings()` is enforced by tests on both sides - // against `flutter/test/fixtures/default_keyboard_shortcuts.json`. - String mainGetDefaultKeyboardShortcuts({dynamic hint}) { - return jsonEncode(kDefaultShortcutBindings); - } - String mainGetInputSource({dynamic hint}) { final inputSource = js.context.callMethod('getByName', ['option:local', 'input-source']); @@ -1192,15 +1176,6 @@ class RustdeskImpl { } Future mainInit({required String appDir, dynamic hint}) { - // JS -> Dart shortcut bridge. The matcher in flutter/web/js/src/ - // shortcut_matcher.ts calls `window.onShortcutTriggered(actionId)` when a - // binding fires; route it to the active session's ShortcutModel. - // Web is single-window so `gFFI` is always the active session. - js.context['onShortcutTriggered'] = (dynamic action) { - if (action is String) { - common.gFFI.shortcutModel.onTriggered(action); - } - }; return Future.value(); } From 6c20fc936d04d0290415ca749cdd624b28969380 Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Thu, 7 May 2026 13:27:13 +0800 Subject: [PATCH 255/277] Terminal utf8 and reconnect (#14895) * fix: handle incomplete UTF-8 sequences in terminal output, rework on https://github.com/rustdesk/rustdesk/pull/14736 * Fix terminal auto-reconnect freeze: reconnect resumes terminal output, while multi-tab reconnect avoids restoring duplicate tabs for terminals that are already open. * fix(terminal): subtract with overflow ``` thread '' panicked at src\server\terminal_service.rs:476:17: attempt to subtract with overflow note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace thread 'tokio-runtime-worker' panicked at src\server\terminal_service.rs:1576:50: called `Result::unwrap()` on an `Err` value: PoisonError { .. } [2026-04-25T07:17:34Z ERROR librustdesk::server::service] Failed to join thread for service ts_9badd3fe-2411-4996-9f40-93c979009edd, Any { .. } ``` Signed-off-by: fufesou * fix ios enter: https://github.com/rustdesk/rustdesk/issues/14907 * fix(terminal): reconnect, error handling 1. Terminal shows "^[[1;1R^[[2;2R^[[>0;0;0c" 2. NaN ``` [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: Converting object to an encodable object failed: NaN ... ``` Signed-off-by: fufesou * fix(terminal): dialog, close window Signed-off-by: fufesou * fix(terminal): close terminal window on disconnect dialog Signed-off-by: fufesou * fix(terminal): merge reconnect backlog into replay output Signed-off-by: fufesou * fix(terminal): avoid reconnect stalls and delayed layout writes Signed-off-by: fufesou * fix(terminal): remove invalid test Signed-off-by: fufesou * fix(terminal): schedule frame before flushing buffered output Signed-off-by: fufesou * fix(terminal): windows&macos, charset utf-8 Signed-off-by: fufesou * fix(terminal): reconnect suppress next output Signed-off-by: fufesou * fix: cap terminal reconnect replay output - split reconnect replay backlog into capped chunks - mark terminal data replay chunks for client-side suppression - avoid using open-message text to suppress xterm replies - reuse default terminal padding value - remove misleading Enter-key normalization PR link Signed-off-by: fufesou * fix(terminal): env en_US.UTF-8 Signed-off-by: fufesou * fix(terminal): reconnect, refactor Signed-off-by: fufesou * fix(terminal): flag, retry output Signed-off-by: fufesou * fix(terminal): update hbb_common Signed-off-by: fufesou * fix(terminal): comments Signed-off-by: fufesou * fix(terminal): comments utf-8 chunk accumulator Signed-off-by: fufesou * fix(terminal): update hbb_common Signed-off-by: fufesou --------- Signed-off-by: fufesou Co-authored-by: fufesou --- flutter/lib/common.dart | 14 +- flutter/lib/desktop/pages/terminal_page.dart | 28 +- .../lib/desktop/pages/terminal_tab_page.dart | 36 +- .../lib/desktop/widgets/tabbar_widget.dart | 1 + flutter/lib/models/terminal_model.dart | 116 +++-- libs/hbb_common | 2 +- src/flutter.rs | 4 + src/server/terminal_helper.rs | 32 +- src/server/terminal_service.rs | 407 ++++++++++++++++-- 9 files changed, 560 insertions(+), 80 deletions(-) diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index e579db36a..366a7b6ba 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -716,6 +716,17 @@ closeConnection({String? id}) { stateGlobal.isInMainPage = true; } else { final controller = Get.find(); + if (controller.tabType == DesktopTabType.terminal && + controller.onCloseWindow != null) { + // Terminal windows are scoped to one peer. The optional id passed to + // closeConnection() is that peer id, not a terminal tab key + // (${peerId}_${terminalId}). Closing from terminal dialogs should close + // the peer's whole terminal window, including all terminal tabs. + unawaited(controller.onCloseWindow!().catchError((e, _) { + debugPrint('[closeConnection] Failed to close terminal window: $e'); + })); + return; + } controller.closeBy(id); } } @@ -4179,8 +4190,7 @@ Widget? buildAvatarWidget({ width: size, height: size, fit: BoxFit.cover, - errorBuilder: (_, __, ___) => - fallback ?? SizedBox.shrink(), + errorBuilder: (_, __, ___) => fallback ?? SizedBox.shrink(), ), ); } diff --git a/flutter/lib/desktop/pages/terminal_page.dart b/flutter/lib/desktop/pages/terminal_page.dart index 0070cd73b..d38dc4a8b 100644 --- a/flutter/lib/desktop/pages/terminal_page.dart +++ b/flutter/lib/desktop/pages/terminal_page.dart @@ -27,6 +27,7 @@ class TerminalPage extends StatefulWidget { final bool? isSharedPassword; final String? connToken; final int terminalId; + /// Tab key for focus management, passed from parent to avoid duplicate construction final String tabKey; final SimpleWrapper?> _lastState = SimpleWrapper(null); @@ -43,6 +44,9 @@ class TerminalPage extends StatefulWidget { class _TerminalPageState extends State with AutomaticKeepAliveClientMixin { + static const EdgeInsets _defaultTerminalPadding = + EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0); + late FFI _ffi; late TerminalModel _terminalModel; double? _cellHeight; @@ -155,13 +159,27 @@ class _TerminalPageState extends State // extra space left after dividing the available height by the height of a single // terminal row (`_cellHeight`) and distributing it evenly as top and bottom padding. EdgeInsets _calculatePadding(double heightPx) { - if (_cellHeight == null) { - return const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0); + final cellHeight = _cellHeight; + if (!heightPx.isFinite || + heightPx <= 0 || + cellHeight == null || + !cellHeight.isFinite || + cellHeight <= 0) { + return _defaultTerminalPadding; + } + final rows = (heightPx / cellHeight).floor(); + if (rows <= 0) { + return _defaultTerminalPadding; + } + final extraSpace = heightPx - rows * cellHeight; + if (!extraSpace.isFinite || extraSpace < 0) { + return _defaultTerminalPadding; } - final rows = (heightPx / _cellHeight!).floor(); - final extraSpace = heightPx - rows * _cellHeight!; final topBottom = extraSpace / 2.0; - return EdgeInsets.symmetric(horizontal: 5.0, vertical: topBottom); + return EdgeInsets.symmetric( + horizontal: _defaultTerminalPadding.horizontal / 2, + vertical: topBottom, + ); } @override diff --git a/flutter/lib/desktop/pages/terminal_tab_page.dart b/flutter/lib/desktop/pages/terminal_tab_page.dart index 28e59fb05..63289e94d 100644 --- a/flutter/lib/desktop/pages/terminal_tab_page.dart +++ b/flutter/lib/desktop/pages/terminal_tab_page.dart @@ -46,6 +46,7 @@ class _TerminalTabPageState extends State { .setTitle(getWindowNameWithId(id)); }; tabController.onRemoved = (_, id) => onRemoveId(id); + tabController.onCloseWindow = _closeWindowFromConnection; final terminalId = params['terminalId'] ?? _nextTerminalId++; tabController.add(_createTerminalTab( peerId: params['id'], @@ -144,6 +145,8 @@ class _TerminalTabPageState extends State { _windowClosing = true; final tabKeys = tabController.state.value.tabs.map((t) => t.key).toList(); // Remove all UI tabs immediately (same instant behavior as the old tabController.clear()) + // Keep the cleanup target lookup below synchronous before its first await: + // it relies on the current frame still retaining each TerminalPage's FFI/model. tabController.clear(); // Run session cleanup in parallel with bounded timeout (closeTerminal() has internal 3s timeout). // Skip tabs already being closed by a concurrent _closeTab() to avoid duplicate FFI calls. @@ -368,8 +371,34 @@ class _TerminalTabPageState extends State { final persistentSessions = args['persistent_sessions'] as List? ?? []; final sortedSessions = persistentSessions.whereType().toList()..sort(); + var peerId = args['peer_id'] as String? ?? ''; + if (peerId.isEmpty) { + if (tabController.state.value.tabs.isEmpty || + tabController.state.value.selected >= + tabController.state.value.tabs.length) { + debugPrint('[TerminalTabPage] Skip restore: no selected tab'); + return; + } + final currentTab = tabController.state.value.selectedTabInfo; + final parsed = _parseTabKey(currentTab.key); + if (parsed == null) return; + peerId = parsed.$1; + } + final existingTerminalIds = tabController.state.value.tabs + .map((tab) => _parseTabKey(tab.key)) + .where((parsed) => parsed != null && parsed.$1 == peerId) + .map((parsed) => parsed!.$2) + .toSet(); + if (existingTerminalIds.isEmpty) { + debugPrint( + '[TerminalTabPage] Skip restore: no seed tab for peer $peerId'); + return; + } for (final terminalId in sortedSessions) { - _addNewTerminalForCurrentPeer(terminalId: terminalId); + if (!existingTerminalIds.add(terminalId)) { + continue; + } + _addNewTerminal(peerId, terminalId: terminalId); // A delay is required to ensure the UI has sufficient time to update // before adding the next terminal. Without this delay, `_TerminalPageState::dispose()` // may be called prematurely while the tab widget is still in the tab controller. @@ -546,6 +575,11 @@ class _TerminalTabPageState extends State { } } + Future _closeWindowFromConnection() async { + await _closeAllTabs(); + await WindowController.fromWindowId(windowId()).close(); + } + int windowId() { return widget.params["windowId"]; } diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index ac7d80017..ef195b493 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -99,6 +99,7 @@ class DesktopTabController { /// index, key Function(int, String)? onRemoved; Function(String)? onSelected; + Future Function()? onCloseWindow; DesktopTabController( {required this.tabType, this.onRemoved, this.onSelected}); diff --git a/flutter/lib/models/terminal_model.dart b/flutter/lib/models/terminal_model.dart index a74241ccb..8961d2dd8 100644 --- a/flutter/lib/models/terminal_model.dart +++ b/flutter/lib/models/terminal_model.dart @@ -27,25 +27,30 @@ class TerminalModel with ChangeNotifier { // Buffer for output data received before terminal view has valid dimensions. // This prevents NaN errors when writing to terminal before layout is complete. final _pendingOutputChunks = []; + final _pendingOutputSuppressFlags = []; int _pendingOutputSize = 0; static const int _kMaxOutputBufferChars = 8 * 1024; // View ready state: true when terminal has valid dimensions, safe to write bool _terminalViewReady = false; - - bool get isPeerWindows => parent.ffiModel.pi.platform == kPeerPlatformWindows; + bool _markViewReadyScheduled = false; + bool _suppressTerminalOutput = false; + bool _suppressNextTerminalDataOutput = false; void Function(int w, int h, int pw, int ph)? onResizeExternal; Future _handleInput(String data) async { - // If we press the `Enter` button on Android, - // `data` can be '\r' or '\n' when using different keyboards. - // Android -> Windows. '\r' works, but '\n' does not. '\n' is just a newline. - // Android -> Linux. Both '\r' and '\n' work as expected (execute a command). - // So when we receive '\n', we may need to convert it to '\r' to ensure compatibility. - // Desktop -> Desktop works fine. - // Check if we are on mobile or web(mobile), and convert '\n' to '\r'. + // Soft keyboards (notably iOS) emit '\n' when Enter is pressed, while a + // real keyboard's Enter sends '\r'. Some Android keyboards also emit '\n'. + // - Peer Windows: '\r' works, '\n' is just a newline. + // - Peer Linux: canonical-mode shells accept both, but raw-mode apps + // (readline, prompt_toolkit, vim, TUI frameworks) expect '\r'. + // - Peer macOS: same as Linux, raw-mode apps expect '\r' + // (https://github.com/rustdesk/rustdesk/issues/14907). + // So on mobile / web-mobile, always normalize a lone '\n' to '\r'. + // We deliberately do not touch multi-character payloads (e.g. pasted text) + // so embedded newlines in pasted content are preserved. final isMobileOrWebMobile = (isMobile || (isWeb && !isWebDesktop)); - if (isMobileOrWebMobile && isPeerWindows && data == '\n') { + if (isMobileOrWebMobile && data == '\n') { data = '\r'; } if (_terminalOpened) { @@ -70,7 +75,10 @@ class TerminalModel with ChangeNotifier { terminalController = TerminalController(); // Setup terminal callbacks - terminal.onOutput = _handleInput; + terminal.onOutput = (data) { + if (_suppressTerminalOutput) return; + _handleInput(data); + }; terminal.onResize = (w, h, pw, ph) async { // Validate all dimensions before using them @@ -84,7 +92,7 @@ class TerminalModel with ChangeNotifier { // Mark terminal view as ready and flush any buffered output on first valid resize. // Must be after onResizeExternal so the view layer has valid dimensions before flushing. if (!_terminalViewReady) { - _markViewReady(); + _scheduleMarkViewReady(); } if (_terminalOpened) { @@ -110,14 +118,16 @@ class TerminalModel with ChangeNotifier { void onReady() { parent.dialogManager.dismissAll(); - // Fire and forget - don't block onReady - openTerminal().catchError((e) { + // Fire and forget - don't block onReady. If the transport reconnects while + // this model is still open, re-send OpenTerminal so the remote service marks + // the persistent session active again and resumes output streaming. + openTerminal(force: _terminalOpened).catchError((e) { debugPrint('[TerminalModel] Error opening terminal: $e'); }); } - Future openTerminal() async { - if (_terminalOpened) return; + Future openTerminal({bool force = false}) async { + if (_terminalOpened && !force) return; // Request the remote side to open a terminal with default shell // The remote side will decide which shell to use based on its OS @@ -275,9 +285,12 @@ class TerminalModel with ChangeNotifier { if (success) { _terminalOpened = true; - // On reconnect ("Reconnected to existing terminal"), server may replay recent output. - // If this TerminalView instance is reused (not rebuilt), duplicate lines can appear. - // We intentionally accept this tradeoff for now to keep logic simple. + // On reconnect, the server may replay recent output. That replay can include + // terminal queries like DSR/DA; xterm answers them through onOutput as + // "^[[1;1R^[[2;2R^[[>0;0;0c", which must not be sent back to the peer. + final replayTerminalOutput = evt['replay_terminal_output']; + _suppressNextTerminalDataOutput = replayTerminalOutput == true || + message == 'Reconnected to existing terminal with pending output'; // Fallback: if terminal view is not yet ready but already has valid // dimensions (e.g. layout completed before open response arrived), @@ -285,7 +298,7 @@ class TerminalModel with ChangeNotifier { if (!_terminalViewReady && terminal.viewWidth > 0 && terminal.viewHeight > 0) { - _markViewReady(); + _scheduleMarkViewReady(); } // Process any buffered input @@ -297,12 +310,16 @@ class TerminalModel with ChangeNotifier { }); final persistentSessions = - evt['persistent_sessions'] as List? ?? []; + (evt['persistent_sessions'] as List? ?? []) + .whereType() + .where((id) => !parent.terminalModels.containsKey(id)) + .toList(); if (kWindowId != null && persistentSessions.isNotEmpty) { DesktopMultiWindow.invokeMethod( kWindowId!, kWindowEventRestoreTerminalSessions, jsonEncode({ + 'peer_id': id, 'persistent_sessions': persistentSessions, })); } @@ -332,6 +349,8 @@ class TerminalModel with ChangeNotifier { final data = evt['data']; if (data != null) { + final suppressTerminalOutput = _suppressNextTerminalDataOutput; + _suppressNextTerminalDataOutput = false; try { String text = ''; if (data is String) { @@ -351,7 +370,7 @@ class TerminalModel with ChangeNotifier { return; } - _writeToTerminal(text); + _writeToTerminal(text, suppressTerminalOutput: suppressTerminalOutput); } catch (e) { debugPrint('[TerminalModel] Failed to process terminal data: $e'); } @@ -361,7 +380,10 @@ class TerminalModel with ChangeNotifier { /// Write text to terminal, buffering if the view is not yet ready. /// All terminal output should go through this method to avoid NaN errors /// from writing before the terminal view has valid layout dimensions. - void _writeToTerminal(String text) { + void _writeToTerminal( + String text, { + bool suppressTerminalOutput = false, + }) { if (!_terminalViewReady) { // If a single chunk exceeds the cap, keep only its tail. // Note: truncation may split a multi-byte ANSI escape sequence, @@ -373,34 +395,73 @@ class TerminalModel with ChangeNotifier { _pendingOutputChunks ..clear() ..add(truncated); + _pendingOutputSuppressFlags + ..clear() + ..add(suppressTerminalOutput); _pendingOutputSize = truncated.length; } else { _pendingOutputChunks.add(text); + _pendingOutputSuppressFlags.add(suppressTerminalOutput); _pendingOutputSize += text.length; // Drop oldest chunks if exceeds limit (whole chunks to preserve ANSI sequences) while (_pendingOutputSize > _kMaxOutputBufferChars && _pendingOutputChunks.length > 1) { final removed = _pendingOutputChunks.removeAt(0); + _pendingOutputSuppressFlags.removeAt(0); _pendingOutputSize -= removed.length; } } return; } - terminal.write(text); + _writeTerminalChunk(text, suppressTerminalOutput: suppressTerminalOutput); } void _flushOutputBuffer() { if (_pendingOutputChunks.isEmpty) return; debugPrint( '[TerminalModel] Flushing $_pendingOutputSize buffered chars (${_pendingOutputChunks.length} chunks)'); - for (final chunk in _pendingOutputChunks) { - terminal.write(chunk); + for (var i = 0; i < _pendingOutputChunks.length; i++) { + _writeTerminalChunk( + _pendingOutputChunks[i], + suppressTerminalOutput: _pendingOutputSuppressFlags[i], + ); } _pendingOutputChunks.clear(); + _pendingOutputSuppressFlags.clear(); _pendingOutputSize = 0; } + void _writeTerminalChunk( + String text, { + required bool suppressTerminalOutput, + }) { + if (!suppressTerminalOutput) { + terminal.write(text); + return; + } + final previous = _suppressTerminalOutput; + _suppressTerminalOutput = true; + try { + terminal.write(text); + } finally { + _suppressTerminalOutput = previous; + } + } + /// Mark terminal view as ready and flush buffered output. + void _scheduleMarkViewReady() { + if (_disposed || _terminalViewReady || _markViewReadyScheduled) return; + _markViewReadyScheduled = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + _markViewReadyScheduled = false; + if (_disposed || _terminalViewReady) return; + if (terminal.viewWidth > 0 && terminal.viewHeight > 0) { + _markViewReady(); + } + }); + WidgetsBinding.instance.ensureVisualUpdate(); + } + void _markViewReady() { if (_terminalViewReady) return; _terminalViewReady = true; @@ -426,7 +487,10 @@ class TerminalModel with ChangeNotifier { // Clear buffers to free memory _inputBuffer.clear(); _pendingOutputChunks.clear(); + _pendingOutputSuppressFlags.clear(); _pendingOutputSize = 0; + _markViewReadyScheduled = false; + _suppressNextTerminalDataOutput = false; // Terminal cleanup is handled server-side when service closes super.dispose(); } diff --git a/libs/hbb_common b/libs/hbb_common index 3e31a9493..42af0f0ae 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 3e31a94939e026ab2c05d21a2c436960aa9bfea8 +Subproject commit 42af0f0aed0bb5fd5df4ff95fd4cc9816fcf5769 diff --git a/src/flutter.rs b/src/flutter.rs index c7e07f892..f8b04bf6c 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1135,6 +1135,10 @@ impl InvokeUiSession for FlutterHandler { ("message", json!(&opened.message)), ("pid", json!(opened.pid)), ("service_id", json!(&opened.service_id)), + ( + "replay_terminal_output", + json!(opened.replay_terminal_output), + ), ]; if !opened.persistent_sessions.is_empty() { event_data.push(("persistent_sessions", json!(opened.persistent_sessions))); diff --git a/src/server/terminal_helper.rs b/src/server/terminal_helper.rs index 8edf4621b..fd85d2a4c 100644 --- a/src/server/terminal_helper.rs +++ b/src/server/terminal_helper.rs @@ -318,6 +318,35 @@ pub fn get_default_shell() -> String { std::env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".to_string()) } +fn utf8_shell_args(shell: &str) -> Vec { + let name = std::path::Path::new(shell) + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(shell) + .to_ascii_lowercase(); + + if name == "cmd.exe" || name == "cmd" { + return vec!["/K".to_string(), "chcp 65001 >NUL".to_string()]; + } + + if name == "pwsh.exe" || name == "pwsh" || name == "powershell.exe" { + return vec![ + "-NoLogo".to_string(), + "-NoExit".to_string(), + "-Command".to_string(), + "chcp.com 65001 > $null; [Console]::InputEncoding = [System.Text.Encoding]::UTF8; [Console]::OutputEncoding = [System.Text.Encoding]::UTF8".to_string(), + ]; + } + + Vec::new() +} + +pub fn configure_utf8_shell_command(shell: &str, cmd: &mut CommandBuilder) { + for arg in utf8_shell_args(shell) { + cmd.arg(arg); + } +} + /// Get the SID of the user from a token. /// Returns a Vec containing the SID bytes. pub fn get_user_sid_from_token(user_token: UserToken) -> Result> { @@ -831,7 +860,8 @@ pub fn run_terminal_helper(args: &[String]) -> Result<()> { let shell = get_default_shell(); log::debug!("Using shell: {}", shell); - let cmd = CommandBuilder::new(&shell); + let mut cmd = CommandBuilder::new(&shell); + configure_utf8_shell_command(&shell, &mut cmd); let mut child = pty_pair .slave .spawn_command(cmd) diff --git a/src/server/terminal_service.rs b/src/server/terminal_service.rs index fb6b4fd29..52a296b74 100644 --- a/src/server/terminal_service.rs +++ b/src/server/terminal_service.rs @@ -20,10 +20,11 @@ use std::{ // Windows-specific imports from terminal_helper module #[cfg(target_os = "windows")] use super::terminal_helper::{ - create_named_pipe_server, encode_helper_message, encode_resize_message, - is_helper_process_running, launch_terminal_helper_with_token, wait_for_pipe_connection, - HelperProcessGuard, OwnedHandle, SendableHandle, WinCloseHandle, WinTerminateProcess, - WinWaitForSingleObject, MSG_TYPE_DATA, PIPE_CONNECTION_TIMEOUT_MS, WIN_WAIT_OBJECT_0, + configure_utf8_shell_command, create_named_pipe_server, encode_helper_message, + encode_resize_message, is_helper_process_running, launch_terminal_helper_with_token, + wait_for_pipe_connection, HelperProcessGuard, OwnedHandle, SendableHandle, WinCloseHandle, + WinTerminateProcess, WinWaitForSingleObject, MSG_TYPE_DATA, PIPE_CONNECTION_TIMEOUT_MS, + WIN_WAIT_OBJECT_0, }; const MAX_OUTPUT_BUFFER_SIZE: usize = 1024 * 1024; // 1MB per terminal @@ -133,6 +134,26 @@ fn get_default_shell() -> String { } } +#[cfg(target_os = "macos")] +fn locale_value_is_utf8(value: &str) -> bool { + let value = value.to_ascii_uppercase(); + value.contains("UTF-8") || value.contains("UTF8") +} + +#[cfg(target_os = "macos")] +fn should_force_process_utf8_ctype() -> bool { + if let Ok(value) = std::env::var("LC_ALL") { + return !locale_value_is_utf8(&value); + } + if let Ok(value) = std::env::var("LC_CTYPE") { + return !locale_value_is_utf8(&value); + } + if let Ok(value) = std::env::var("LANG") { + return !locale_value_is_utf8(&value); + } + true +} + pub fn is_service_specified_user(service_id: &str) -> Option { get_service(service_id).map(|s| s.lock().unwrap().is_specified_user) } @@ -435,6 +456,7 @@ impl OutputBuffer { // Find first newline in new data if let Some(newline_pos) = data.iter().position(|&b| b == b'\n') { last_line.extend_from_slice(&data[..=newline_pos]); + self.total_size += newline_pos + 1; start = newline_pos + 1; self.last_line_incomplete = false; } else { @@ -473,7 +495,28 @@ impl OutputBuffer { // Trim old data if buffer is too large while self.total_size > MAX_OUTPUT_BUFFER_SIZE || self.lines.len() > MAX_BUFFER_LINES { if let Some(removed) = self.lines.pop_front() { - self.total_size -= removed.len(); + if removed.len() > self.total_size { + log::error!( + "OutputBuffer total_size underflow avoided: total_size={}, removed_len={}, lines_len={}", + self.total_size, + removed.len(), + self.lines.len() + ); + self.total_size = self.lines.iter().map(|line| line.len()).sum(); + } else { + self.total_size -= removed.len(); + } + if self.lines.is_empty() { + self.last_line_incomplete = false; + } + } else { + log::error!( + "OutputBuffer trim invariant broken: total_size={}, lines_len=0", + self.total_size + ); + self.total_size = 0; + self.last_line_incomplete = false; + break; } } } @@ -531,6 +574,97 @@ impl OutputBuffer { } } +/// Find the largest prefix of `buf` that does not end in the middle of a UTF-8 +/// code point. Invalid bytes are treated as complete so they can continue +/// downstream and be rendered with replacement characters if needed. +fn find_utf8_split_point(buf: &[u8]) -> usize { + if buf.is_empty() { + return 0; + } + + let start = buf.len().saturating_sub(3); + for i in (start..buf.len()).rev() { + let b = buf[i]; + if b & 0x80 == 0 { + return buf.len(); + } + if b & 0xC0 == 0x80 { + continue; + } + + let seq_len = if b & 0xE0 == 0xC0 { + 2 + } else if b & 0xF0 == 0xE0 { + 3 + } else if b & 0xF8 == 0xF0 { + 4 + } else { + return buf.len(); + }; + + return if buf.len() - i >= seq_len { + buf.len() + } else { + i + }; + } + + buf.len() +} + +// Terminal output currently follows a UTF-8 text model end to end: the service +// keeps replay buffers on UTF-8 boundaries, and Flutter decodes payload bytes as +// UTF-8 before writing to xterm. This accumulator only prevents splitting a +// trailing UTF-8 code point across PTY reads. Supporting non-UTF-8 terminals +// would need a separate design covering remote encoding detection, Flutter +// decoding, replay truncation, and input transcoding. +#[derive(Default)] +struct Utf8ChunkAccumulator { + remainder: Vec, +} + +impl Utf8ChunkAccumulator { + fn push_chunk(&mut self, mut data: Vec) -> Option> { + if data.is_empty() { + return None; + } + + let had_remainder = !self.remainder.is_empty(); + if had_remainder { + let mut combined = std::mem::take(&mut self.remainder); + combined.extend_from_slice(&data); + data = combined; + } + + let split = find_utf8_split_point(&data); + if split == data.len() { + return Some(data); + } + + // Only hold back a candidate incomplete suffix when we have evidence that + // the bytes before it are already UTF-8 text. If split is 0, the whole + // read may be the start of a UTF-8 character, so keep it for the next read. + if !had_remainder && split > 0 && std::str::from_utf8(&data[..split]).is_err() { + return Some(data); + } + + self.remainder = data.split_off(split); + if data.is_empty() { + None + } else { + Some(data) + } + } + + fn finish(&mut self) -> Option> { + if self.remainder.is_empty() { + None + } else { + Some(std::mem::take(&mut self.remainder)) + } + } +} + /// Try to send data through the output channel with rate-limited drop logging. /// Returns `true` if the caller should break out of the read loop (channel disconnected). fn try_send_output( @@ -570,7 +704,11 @@ fn try_send_output( false } Err(mpsc::TrySendError::Disconnected(_)) => { - log::debug!("Terminal {}{} output channel disconnected", terminal_id, label); + log::debug!( + "Terminal {}{} output channel disconnected", + terminal_id, + label + ); true } } @@ -937,15 +1075,35 @@ impl TerminalServiceProxy { if let Some(session_arc) = service.sessions.get(&open.terminal_id) { // Reconnect to existing terminal let mut session = session_arc.lock().unwrap(); - // Directly enter Active state with pending buffer for immediate streaming. - // Historical buffer is sent first by read_outputs(), then real-time data follows. - // No overlap: pending_buffer comes from output_buffer (pre-disconnect history), - // while received_data in read_outputs() comes from the channel (post-reconnect). - // During disconnect, the run loop (sp.ok()) exits so read_outputs() stops being - // called; output_buffer is not updated, and channel data may be lost if it fills up. - let buffer = session + // Directly enter Active state with pending replay for immediate streaming. + // The replay combines output_buffer history and the channel backlog that was + // already pending at reconnect time so the client can suppress stale xterm + // query answers without requiring a protobuf schema change. + // During disconnect, read_outputs() is not called; channel data can still be lost + // if output_rx fills before reconnect drains it. + let mut buffer = session .output_buffer .get_recent(DEFAULT_RECONNECT_BUFFER_BYTES); + let mut reconnect_backlog = Vec::new(); + if let Some(output_rx) = &session.output_rx { + // Cap reconnect-time drain so a chatty PTY cannot keep OpenTerminal + // inside this loop indefinitely. Remaining output is drained by read_outputs(). + for _ in 0..CHANNEL_BUFFER_SIZE { + let Ok(data) = output_rx.try_recv() else { + break; + }; + reconnect_backlog.push(data); + } + } + let has_reconnect_backlog = !reconnect_backlog.is_empty(); + for data in reconnect_backlog { + session.output_buffer.append(&data); + } + if has_reconnect_backlog { + buffer = session + .output_buffer + .get_recent(DEFAULT_RECONNECT_BUFFER_BYTES); + } let has_pending = !buffer.is_empty(); session.state = SessionState::Active { pending_buffer: if has_pending { Some(buffer) } else { None }, @@ -959,9 +1117,14 @@ impl TerminalServiceProxy { let mut opened = TerminalOpened::new(); opened.terminal_id = open.terminal_id; opened.success = true; - opened.message = "Reconnected to existing terminal".to_string(); + opened.message = if has_pending { + "Reconnected to existing terminal with pending output".to_string() + } else { + "Reconnected to existing terminal".to_string() + }; opened.pid = session.pid; opened.service_id = self.service_id.clone(); + opened.replay_terminal_output = has_pending; if service.needs_session_sync { if service.sessions.len() > 1 { // No need to include the current terminal in the list. @@ -1016,6 +1179,9 @@ impl TerminalServiceProxy { #[allow(unused_mut)] let mut cmd = CommandBuilder::new(&shell); + #[cfg(target_os = "windows")] + configure_utf8_shell_command(&shell, &mut cmd); + // macOS-specific terminal configuration // 1. Use login shell (-l) to load user's shell profile (~/.zprofile, ~/.bash_profile) // This ensures PATH includes Homebrew paths (/opt/homebrew/bin, /usr/local/bin) @@ -1036,6 +1202,12 @@ impl TerminalServiceProxy { }; cmd.env("TERM", term); log::debug!("Set TERM={} for macOS PTY", term); + + if should_force_process_utf8_ctype() { + cmd.env_remove("LC_ALL"); + cmd.env("LC_CTYPE", "en_US.UTF-8"); + log::debug!("Set LC_CTYPE=en_US.UTF-8 for macOS PTY"); + } } // Note: On Windows with user_token, we use helper mode (handle_open_with_helper) @@ -1086,6 +1258,7 @@ impl TerminalServiceProxy { let reader_thread = thread::spawn(move || { let mut reader = reader; let mut buf = vec![0u8; 4096]; + let mut utf8_chunks = Utf8ChunkAccumulator::default(); let mut drop_count: u64 = 0; // Initialize to > 5s ago so the first drop triggers a warning immediately. let mut last_drop_warn = Instant::now() - Duration::from_secs(6); @@ -1095,13 +1268,25 @@ impl TerminalServiceProxy { // EOF // This branch can be reached when the child process exits on macOS. // But not on Linux and Windows in my tests. + if let Some(data) = utf8_chunks.finish() { + let _ = try_send_output( + &output_tx, + data, + terminal_id, + "", + &mut drop_count, + &mut last_drop_warn, + ); + } break; } Ok(n) => { if exiting.load(Ordering::SeqCst) { break; } - let data = buf[..n].to_vec(); + let Some(data) = utf8_chunks.push_chunk(buf[..n].to_vec()) else { + continue; + }; // Use try_send to avoid blocking the reader thread when channel is full. // During disconnect, the run loop (sp.ok()) stops and read_outputs() is // no longer called, so the channel won't be drained. Blocking send would @@ -1308,12 +1493,23 @@ impl TerminalServiceProxy { let terminal_id = open.terminal_id; let reader_thread = thread::spawn(move || { let mut buf = vec![0u8; 4096]; + let mut utf8_chunks = Utf8ChunkAccumulator::default(); let mut drop_count: u64 = 0; // Initialize to > 5s ago so the first drop triggers a warning immediately. let mut last_drop_warn = Instant::now() - Duration::from_secs(6); loop { match output_pipe.read(&mut buf) { Ok(0) => { + if let Some(data) = utf8_chunks.finish() { + let _ = try_send_output( + &output_tx, + data, + terminal_id, + " (helper)", + &mut drop_count, + &mut last_drop_warn, + ); + } // EOF - helper process exited log::debug!("Terminal {} helper output EOF", terminal_id); break; @@ -1322,7 +1518,9 @@ impl TerminalServiceProxy { if exiting.load(Ordering::SeqCst) { break; } - let data = buf[..n].to_vec(); + let Some(data) = utf8_chunks.push_chunk(buf[..n].to_vec()) else { + continue; + }; // Use try_send to avoid blocking the reader thread (same as direct PTY mode) if try_send_output( &output_tx, @@ -1462,20 +1660,28 @@ impl TerminalServiceProxy { data: &TerminalData, ) -> Result> { if let Some(session_arc) = session { - let mut session = session_arc.lock().unwrap(); - session.update_activity(); - if let Some(input_tx) = &session.input_tx { - // Encode data for helper mode or send raw for direct PTY mode - #[cfg(target_os = "windows")] - let msg = if session.is_helper_mode { - encode_helper_message(MSG_TYPE_DATA, &data.data) - } else { - data.data.to_vec() - }; - #[cfg(not(target_os = "windows"))] - let msg = data.data.to_vec(); + let input = { + let mut session = session_arc.lock().unwrap(); + session.update_activity(); + if let Some(input_tx) = session.input_tx.clone() { + // Encode data for helper mode or send raw for direct PTY mode + #[cfg(target_os = "windows")] + let msg = if session.is_helper_mode { + encode_helper_message(MSG_TYPE_DATA, &data.data) + } else { + data.data.to_vec() + }; + #[cfg(not(target_os = "windows"))] + let msg = data.data.to_vec(); - // Send data to writer thread + Some((input_tx, msg)) + } else { + None + } + }; + + if let Some((input_tx, msg)) = input { + // Send outside the session lock; SyncSender::send can block when full. if let Err(e) = input_tx.send(msg) { log::error!( "Failed to send data to terminal {}: {}", @@ -1683,10 +1889,6 @@ impl TerminalServiceProxy { } } - if has_activity { - session.update_activity(); - } - // Update buffer (always buffer for reconnection support) for data in &received_data { session.output_buffer.append(data); @@ -1696,7 +1898,7 @@ impl TerminalServiceProxy { // Data is already buffered above and will be sent on next reconnection. // Use a scoped block to limit the mutable borrow of session.state, // so we can immutably borrow other session fields afterwards. - let sigwinch_action = { + let (replay_buffer, sigwinch_action) = { let (pending_buffer, sigwinch) = match &mut session.state { SessionState::Active { pending_buffer, @@ -1705,19 +1907,12 @@ impl TerminalServiceProxy { _ => continue, }; - // Send pending buffer response first (set on reconnection in handle_open). - // This ensures historical buffer is sent before any real-time data. - if let Some(buffer) = pending_buffer.take() { - if !buffer.is_empty() { - responses - .push(Self::create_terminal_data_response(terminal_id, buffer)); - } - } + let replay_buffer = pending_buffer.take(); // Two-phase SIGWINCH: see SigwinchPhase doc comments for rationale. // Each phase is a single PTY resize, spaced ~30ms apart by the polling // interval, ensuring the TUI app sees a real size change on each signal. - match sigwinch { + let sigwinch_action = match sigwinch { SigwinchPhase::TempResize { retries } => { if *retries == 0 { log::warn!( @@ -1745,9 +1940,20 @@ impl TerminalServiceProxy { } } SigwinchPhase::Idle => None, - } + }; + (replay_buffer, sigwinch_action) }; + if let Some(buffer) = replay_buffer { + if !buffer.is_empty() { + responses.push(Self::create_terminal_data_response(terminal_id, buffer)); + } + } + + if has_activity { + session.update_activity(); + } + // Execute SIGWINCH resize outside the mutable borrow scope of session.state. if let Some(action) = sigwinch_action { #[cfg(target_os = "windows")] @@ -1845,3 +2051,116 @@ impl TerminalServiceProxy { } } } + +#[cfg(test)] +mod tests { + use super::{find_utf8_split_point, OutputBuffer, Utf8ChunkAccumulator, MAX_BUFFER_LINES}; + + #[test] + fn utf8_split_point_returns_full_len_for_complete_input() { + assert_eq!(find_utf8_split_point(b"hello"), 5); + assert_eq!(find_utf8_split_point("中文".as_bytes()), "中文".len()); + assert_eq!(find_utf8_split_point("😀".as_bytes()), "😀".len()); + } + + #[test] + fn utf8_split_point_detects_incomplete_trailing_sequence() { + let data = [b'a', 0xE4, 0xB8]; + assert_eq!(find_utf8_split_point(&data), 1); + } + + #[test] + fn utf8_split_point_keeps_malformed_prefix_but_buffers_trailing_lead_byte() { + let data = [0xFF, 0xE4]; + assert_eq!(find_utf8_split_point(&data), 1); + } + + #[test] + fn utf8_split_point_treats_orphan_continuations_as_complete() { + let data = [0x80, 0x81, 0x82]; + assert_eq!(find_utf8_split_point(&data), data.len()); + } + + #[test] + fn utf8_chunk_accumulator_reassembles_split_multibyte_output() { + let full = "你好世界".as_bytes(); + let mut chunker = Utf8ChunkAccumulator::default(); + let mut output = Vec::new(); + + for chunk in full.chunks(5) { + if let Some(data) = chunker.push_chunk(chunk.to_vec()) { + output.extend_from_slice(&data); + } + } + + if let Some(data) = chunker.finish() { + output.extend_from_slice(&data); + } + + assert_eq!(output, full); + } + + #[test] + fn utf8_chunk_accumulator_buffers_leading_split_multibyte_output() { + let mut chunker = Utf8ChunkAccumulator::default(); + + assert!(chunker.push_chunk(vec![0xE4]).is_none()); + assert!(chunker.push_chunk(vec![0xB8]).is_none()); + assert_eq!( + chunker.push_chunk(vec![0xAD]), + Some("中".as_bytes().to_vec()) + ); + assert!(chunker.finish().is_none()); + } + + #[test] + fn utf8_chunk_accumulator_flushes_incomplete_tail_on_finish() { + let mut chunker = Utf8ChunkAccumulator::default(); + assert_eq!(chunker.push_chunk(vec![b'a', 0xE4]), Some(vec![b'a'])); + assert_eq!(chunker.finish(), Some(vec![0xE4])); + assert!(chunker.finish().is_none()); + } + + #[test] + fn utf8_chunk_accumulator_does_not_stall_on_malformed_bytes() { + let mut chunker = Utf8ChunkAccumulator::default(); + assert_eq!(chunker.push_chunk(vec![0xFF]), Some(vec![0xFF])); + assert!(chunker.finish().is_none()); + } + + #[test] + fn utf8_chunk_accumulator_buffers_lone_utf8_lead_bytes() { + let mut chunker = Utf8ChunkAccumulator::default(); + assert!(chunker.push_chunk(vec![0xE4]).is_none()); + assert_eq!(chunker.finish(), Some(vec![0xE4])); + } + + #[test] + fn utf8_chunk_accumulator_does_not_hold_back_non_utf8_prefixes() { + let mut chunker = Utf8ChunkAccumulator::default(); + assert_eq!(chunker.push_chunk(vec![0xFF, 0xE4]), Some(vec![0xFF, 0xE4])); + assert!(chunker.finish().is_none()); + } + + #[test] + fn output_buffer_trim_after_incomplete_merge_does_not_underflow() { + let mut buffer = OutputBuffer::new(); + + // Create an incomplete line first. + buffer.append(b"hello"); + + // Merge a large chunk that contains the first newline at the tail. + // This exercises the "append to last incomplete line" branch. + let mut large = vec![b'a'; 30_000]; + large.push(b'\n'); + buffer.append(&large); + + // Exceed MAX_BUFFER_LINES so trim pops the first large merged line. + for _ in 0..=MAX_BUFFER_LINES { + buffer.append(b"x\n"); + } + + let actual_size: usize = buffer.lines.iter().map(|line| line.len()).sum(); + assert_eq!(buffer.total_size, actual_size); + } +} From 72d27c3c47b0d081ec35aedbadf226d7a1b9bf0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?VenusGirl=E2=9D=A4?= Date: Fri, 8 May 2026 18:49:17 +0900 Subject: [PATCH 256/277] Update Korean (#14956) --- src/lang/ko.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 7b3ffd98e..de68574e1 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -743,6 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "표시 이름"), ("password-hidden-tip", "영구 비밀번호가 설정되었습니다 (숨김)."), ("preset-password-in-use-tip", "현재 사전 설정된 비밀번호가 사용 중입니다."), - ("Enable privacy mode", ""), + ("Enable privacy mode", "개인정보 보호 모드 사용함"), ].iter().cloned().collect(); } From 9df486a689dbee26ba9868c68131d6a627018fba Mon Sep 17 00:00:00 2001 From: fufesou Date: Sat, 9 May 2026 18:15:00 +0800 Subject: [PATCH 257/277] fix(ipc): harden local IPC authorization and portable-service bootstrap flow (#14671) * fix(ipc): harden ipc access Signed-off-by: fufesou * fix(ipc): full cmd path, comments, simple refactor Signed-off-by: fufesou * fix(ipc): portable service, ipc exit Signed-off-by: fufesou * fix(ipc): Remove unused logs Signed-off-by: fufesou * fix(ipc): Use SetEntriesInAclW instead of icacls Signed-off-by: fufesou * fix(ipc): Comments Signed-off-by: fufesou * fix(ipc): check is_reparse_point Signed-off-by: fufesou * fix(ipc): shmem name, no fallback Signed-off-by: fufesou * fix(ipc): Simple refactor Signed-off-by: fufesou * fix(ipc): better exit and clear Signed-off-by: fufesou * fix(ipc): portable service, better exit Signed-off-by: fufesou * fix(ipc): comments, id -u Signed-off-by: fufesou * fix: comments linux headless, rx desktop ready Signed-off-by: fufesou * fix(ipc): magic number Signed-off-by: fufesou * fix(ipc): update deps Signed-off-by: fufesou * Update Cargo.lock * Update Cargo.lock * fix(ipc): harden ipc, test `identity_unavailable` Signed-off-by: fufesou * fix(ipc): portable service, check dir of shmem Signed-off-by: fufesou * fix(ipc): macos, better check exe allowed Signed-off-by: fufesou * fix(ipc): update hbb_common Signed-off-by: fufesou * fix(ipc): update hbb_common Signed-off-by: fufesou * fix(ipc): harden ipc, better active uid for uinput Signed-off-by: fufesou * fix(ipc): harden portable service token validation Compare portable service IPC tokens in constant time and document the CSPRNG source used for one-time token generation. Clarify Windows IPC authorization comments around canonical path matching and partial peer identity lookup. Signed-off-by: fufesou * fix(ipc): simple refactor Signed-off-by: fufesou * fix(ipc): harden portable service token handling Generate the portable service IPC token directly from OsRng, keep token comparison in the IPC layer as a fixed-length byte-wise check, and document the malformed-frame behavior for protected service IPC. Signed-off-by: fufesou * fix(ipc): comments Signed-off-by: fufesou --------- Signed-off-by: fufesou Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> --- Cargo.lock | 4 +- src/core_main.rs | 8 +- src/ipc.rs | 467 +++++++++++--- src/ipc/auth.rs | 1036 ++++++++++++++++++++++++++++++++ src/ipc/fs.rs | 951 +++++++++++++++++++++++++++++ src/platform/linux.rs | 51 ++ src/platform/windows.rs | 320 +++++++++- src/platform/windows/acl.rs | 903 ++++++++++++++++++++++++++++ src/server.rs | 10 +- src/server/connection.rs | 162 +++-- src/server/portable_service.rs | 790 +++++++++++++++++++++--- src/server/uinput.rs | 47 +- 12 files changed, 4500 insertions(+), 249 deletions(-) create mode 100644 src/ipc/auth.rs create mode 100644 src/ipc/fs.rs create mode 100644 src/platform/windows/acl.rs diff --git a/Cargo.lock b/Cargo.lock index febfd6b17..fe1f67cc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5996,8 +5996,8 @@ dependencies = [ [[package]] name = "parity-tokio-ipc" -version = "0.7.3-5" -source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#c8c8bbcbabf9be1201c53afb0269b92b9b02d291" +version = "0.7.3-6" +source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#d0ae39bffe5d5a3e8d82a1b6bcb1ca5a9b2f1c01" dependencies = [ "futures", "libc", diff --git a/src/core_main.rs b/src/core_main.rs index e27091927..67a83a37e 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -146,7 +146,13 @@ pub fn core_main() -> Option> { crate::portable_service::client::set_quick_support(_is_quick_support); } let mut log_name = "".to_owned(); - if args.len() > 0 && args[0].starts_with("--") { + // Keep portable-service logs under a stable directory name. + let has_portable_service_shmem_arg = args + .iter() + .any(|arg| arg.starts_with("--portable-service-shmem-name=")); + if has_portable_service_shmem_arg { + log_name = "portable-service".to_owned(); + } else if args.len() > 0 && args[0].starts_with("--") { let name = args[0].replace("--", ""); if !name.is_empty() { log_name = name; diff --git a/src/ipc.rs b/src/ipc.rs index 82b52a60c..0258a2816 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -1,33 +1,28 @@ -use crate::{ - common::CheckTestNatType, - privacy_mode::PrivacyModeState, - ui_interface::{get_local_option, set_local_option}, -}; -use bytes::Bytes; -use parity_tokio_ipc::{ - Connection as Conn, ConnectionClient as ConnClient, Endpoint, Incoming, SecurityAttributes, -}; -use serde_derive::{Deserialize, Serialize}; -use std::{ - collections::HashMap, - sync::atomic::{AtomicBool, Ordering}, -}; -#[cfg(not(windows))] -use std::{fs::File, io::prelude::*}; +#[path = "ipc/auth.rs"] +mod ipc_auth; +#[cfg(any(target_os = "linux", target_os = "macos"))] +#[path = "ipc/fs.rs"] +mod ipc_fs; #[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::plugin::ipc::Plugin; +use crate::{ + common::{is_server, CheckTestNatType}, + privacy_mode, + privacy_mode::PrivacyModeState, + rendezvous_mediator::RendezvousMediator, + ui_interface::{get_local_option, set_local_option}, +}; +use bytes::Bytes; #[cfg(not(any(target_os = "android", target_os = "ios")))] pub use clipboard::ClipboardFile; +#[cfg(target_os = "linux")] +use hbb_common::anyhow; use hbb_common::{ allow_err, bail, bytes, bytes_codec::BytesCodec, - config::{ - self, - keys::{self, OPTION_ALLOW_WEBSOCKET}, - Config, Config2, - }, + config::{self, keys::OPTION_ALLOW_WEBSOCKET, Config, Config2}, futures::StreamExt as _, futures_util::sink::SinkExt, log, password_security as password, timeout, @@ -38,13 +33,55 @@ use hbb_common::{ tokio_util::codec::Framed, ResultType, }; - -use crate::{common::is_server, privacy_mode, rendezvous_mediator::RendezvousMediator}; +#[cfg(any(target_os = "linux", target_os = "macos"))] +use ipc_auth::authorize_service_scoped_ipc_connection; +#[cfg(windows)] +pub(crate) use ipc_auth::authorize_windows_portable_service_ipc_connection; +#[cfg(windows)] +pub(crate) use ipc_auth::ensure_peer_executable_matches_current_by_pid_opt; +#[cfg(windows)] +pub(crate) use ipc_auth::log_rejected_windows_ipc_connection; +#[cfg(target_os = "linux")] +pub(crate) use ipc_auth::{ + active_uid, ensure_peer_executable_matches_current_by_fd, is_allowed_service_peer_uid, + log_rejected_uinput_connection, peer_uid_from_fd, +}; +#[cfg(windows)] +use ipc_auth::{ + authorize_windows_main_ipc_connection, portable_service_listener_security_attributes, + should_allow_everyone_create_on_windows, +}; +#[cfg(target_os = "linux")] +use ipc_fs::terminal_count_candidate_uids; +#[cfg(any(target_os = "linux", target_os = "macos"))] +use ipc_fs::{ + check_pid, ensure_secure_ipc_parent_dir, scrub_secure_ipc_parent_dir, + should_scrub_parent_entries_after_check_pid, write_pid, +}; +use parity_tokio_ipc::{ + Connection as Conn, ConnectionClient as ConnClient, Endpoint, Incoming, SecurityAttributes, +}; +use serde_derive::{Deserialize, Serialize}; +#[cfg(any(target_os = "linux", target_os = "macos"))] +use std::os::unix::fs::PermissionsExt; +use std::{ + collections::HashMap, + sync::atomic::{AtomicBool, Ordering}, +}; // IPC actions here. pub const IPC_ACTION_CLOSE: &str = "close"; +const PORTABLE_SERVICE_IPC_HANDSHAKE_TIMEOUT_MS: u64 = 3_000; +pub(crate) const IPC_TOKEN_LEN: usize = 64; +const IPC_TOKEN_RANDOM_BYTES: usize = IPC_TOKEN_LEN / 2; +const _: () = assert!(IPC_TOKEN_LEN % 2 == 0); pub static EXIT_RECV_CLOSE: AtomicBool = AtomicBool::new(true); +#[inline] +pub async fn connect_service(ms_timeout: u64) -> ResultType> { + connect(ms_timeout, crate::POSTFIX_SERVICE).await +} + #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(tag = "t", content = "c")] pub enum FS { @@ -207,6 +244,8 @@ pub enum DataControl { pub enum DataPortableService { Ping, Pong, + AuthToken(String), + AuthResult(bool), ConnCount(Option), Mouse((Vec, i32, String, u32, bool, bool)), Pointer((Vec, i32)), @@ -411,6 +450,22 @@ pub async fn start(postfix: &str) -> ResultType<()> { Ok(stream) => { let mut stream = Connection::new(stream); let postfix = postfix.to_owned(); + #[cfg(any(target_os = "linux", target_os = "macos"))] + if config::is_service_ipc_postfix(&postfix) { + if !authorize_service_scoped_ipc_connection(&stream, &postfix) { + continue; + } + } + #[cfg(windows)] + if postfix.is_empty() { + // Windows main IPC (`postfix == ""`) is authorized here. + // Other security-sensitive channels use dedicated authorization paths: + // - `_portable_service`: portable-service listener + handshake policy + // - service-scoped postfixes: service-specific listener/authorization + if !authorize_windows_main_ipc_connection(&stream, &postfix) { + continue; + } + } tokio::spawn(async move { loop { match stream.next().await { @@ -419,9 +474,48 @@ pub async fn start(postfix: &str) -> ResultType<()> { break; } Ok(Some(data)) => { + // On Linux/macOS, the protected `_service` channel is used only for + // syncing config between root service and the active user process. + // + // NOTE: `is_service_ipc_postfix()` also includes `_uinput_*`, but those + // channels are handled by the dedicated uinput listener/protocol in + // `src/server/uinput.rs` and therefore do not share this Data enum + // allowlist. The SyncConfig allowlist here is intentionally scoped to the + // `_service` channel only. + // + // Keep this explicit branch to avoid policy drift between `_service` and + // uinput IPC paths while still minimizing exposed message surface here. + #[cfg(any(target_os = "linux", target_os = "macos"))] + if postfix == crate::POSTFIX_SERVICE { + if matches!(&data, Data::SyncConfig(_)) { + handle(data, &mut stream).await; + } else { + log::warn!( + "Rejected non-sync data on protected _service IPC channel: postfix={}, data_kind={:?}, peer_uid={:?}", + postfix, + std::mem::discriminant(&data), + stream.peer_uid() + ); + // Close the connection to avoid keeping a protected channel + // alive while repeatedly receiving invalid traffic. + break; + } + continue; + } handle(data, &mut stream).await; } - _ => {} + Ok(None) => { + // `Ok(None)` means a complete frame arrived but did not + // deserialize into `Data`. Peer close/reset is returned as + // `Err` by `ConnectionTmpl::next()`. Keep the historical + // ignore behavior except on the protected `_service` channel. + #[cfg(any(target_os = "linux", target_os = "macos"))] + { + if postfix == crate::POSTFIX_SERVICE { + break; + } + } + } } } }); @@ -436,20 +530,77 @@ pub async fn start(postfix: &str) -> ResultType<()> { pub async fn new_listener(postfix: &str) -> ResultType { let path = Config::ipc_path(postfix); - #[cfg(not(any(windows, target_os = "android", target_os = "ios")))] - check_pid(postfix).await; + #[cfg(any(target_os = "linux", target_os = "macos"))] + let should_scrub_parent_entries = ensure_secure_ipc_parent_dir(&path, postfix)?; + #[cfg(any(target_os = "linux", target_os = "macos"))] + let existing_listener_alive = check_pid(postfix).await; + #[cfg(any(target_os = "linux", target_os = "macos"))] + if should_scrub_parent_entries_after_check_pid( + should_scrub_parent_entries, + existing_listener_alive, + ) { + scrub_secure_ipc_parent_dir(&path, postfix)?; + } let mut endpoint = Endpoint::new(path.clone()); - match SecurityAttributes::allow_everyone_create() { + let security_attrs = { + #[cfg(windows)] + { + if postfix == "_portable_service" { + portable_service_listener_security_attributes() + } else if should_allow_everyone_create_on_windows(postfix) { + SecurityAttributes::allow_everyone_create() + } else { + Ok(SecurityAttributes::empty()) + } + } + #[cfg(not(windows))] + { + SecurityAttributes::allow_everyone_create() + } + }; + match security_attrs { Ok(attr) => endpoint.set_security_attributes(attr), - Err(err) => log::error!("Failed to set ipc{} security: {}", postfix, err), + Err(err) => { + log::error!("Failed to set ipc{} security: {}", postfix, err); + #[cfg(windows)] + if postfix == "_portable_service" { + // Fail closed for `_portable_service` when SDDL construction fails. + // This endpoint is security-critical and must not start with default ACLs. + return Err(err.into()); + } + } }; match endpoint.incoming() { Ok(incoming) => { - log::info!("Started ipc{} server at path: {}", postfix, &path); - #[cfg(not(windows))] + if postfix == crate::POSTFIX_SERVICE { + log::info!("Started protected ipc service server: postfix={}", postfix); + } else { + log::info!("Started ipc{} server at path: {}", postfix, &path); + } + #[cfg(any(target_os = "linux", target_os = "macos"))] { - use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o0777)).ok(); + // NOTE: On Linux/macOS, some IPC sockets are intentionally world-connectable + // (0666) so the active (non-root) user process can connect. Authorization is + // enforced at accept-time for these channels, and the protected `_service` + // channel is further restricted by an explicit message allowlist (SyncConfig + // only). + let socket_mode = if config::is_service_ipc_postfix(postfix) { + 0o0666 + } else { + 0o0600 + }; + if let Err(err) = + std::fs::set_permissions(&path, std::fs::Permissions::from_mode(socket_mode)) + { + log::error!( + "Failed to set permissions on ipc{} socket at path {}: {}", + postfix, + &path, + err + ); + std::fs::remove_file(&path).ok(); + return Err(err.into()); + } write_pid(postfix); } Ok(incoming) @@ -953,15 +1104,116 @@ async fn handle(data: Data, stream: &mut Connection) { ); } _ => {} - } + }; } pub async fn connect(ms_timeout: u64, postfix: &str) -> ResultType> { let path = Config::ipc_path(postfix); - let client = timeout(ms_timeout, Endpoint::connect(&path)).await??; + connect_with_path(ms_timeout, &path).await +} + +pub(crate) fn generate_one_time_ipc_token() -> ResultType { + use hbb_common::rand::{rngs::OsRng, RngCore as _}; + use std::fmt::Write as _; + + let mut random_bytes = [0u8; IPC_TOKEN_RANDOM_BYTES]; + let mut rng = OsRng; + rng.try_fill_bytes(&mut random_bytes).map_err(|err| { + hbb_common::anyhow::anyhow!( + "failed to generate portable service ipc token from OsRng: {}", + err + ) + })?; + + let mut token = String::with_capacity(IPC_TOKEN_LEN); + for byte in random_bytes { + let _ = write!(token, "{:02x}", byte); + } + Ok(token) +} + +pub(crate) fn constant_time_ipc_token_eq(expected: &str, candidate: &str) -> bool { + if expected.len() != IPC_TOKEN_LEN || candidate.len() != IPC_TOKEN_LEN { + return false; + } + expected + .as_bytes() + .iter() + .zip(candidate.as_bytes().iter()) + .fold(0u8, |diff, (left, right)| diff | (*left ^ *right)) + == 0 +} + +pub(crate) async fn portable_service_ipc_handshake_as_client( + stream: &mut ConnectionTmpl, + token: &str, +) -> ResultType<()> +where + T: AsyncRead + AsyncWrite + std::marker::Unpin, +{ + stream + .send(&Data::DataPortableService(DataPortableService::AuthToken( + token.to_owned(), + ))) + .await?; + match stream + .next_timeout(PORTABLE_SERVICE_IPC_HANDSHAKE_TIMEOUT_MS) + .await? + { + Some(Data::DataPortableService(DataPortableService::AuthResult(true))) => Ok(()), + Some(Data::DataPortableService(DataPortableService::AuthResult(false))) => { + bail!("portable service ipc handshake was rejected by server") + } + Some(_) | None => bail!("portable service ipc handshake returned an unexpected response"), + } +} + +pub(crate) async fn portable_service_ipc_handshake_as_server( + stream: &mut ConnectionTmpl, + mut validate_token: F, +) -> ResultType<()> +where + T: AsyncRead + AsyncWrite + std::marker::Unpin, + // Token validators must use `constant_time_ipc_token_eq` or an equivalent + // fixed-length comparison; this handshake is part of the privilege boundary. + F: FnMut(&str) -> bool, +{ + let authorized = match stream + .next_timeout(PORTABLE_SERVICE_IPC_HANDSHAKE_TIMEOUT_MS) + .await? + { + Some(Data::DataPortableService(DataPortableService::AuthToken(token))) => { + validate_token(&token) + } + Some(_) | None => false, + }; + stream + .send(&Data::DataPortableService(DataPortableService::AuthResult( + authorized, + ))) + .await?; + if !authorized { + bail!("portable service ipc handshake failed") + } + Ok(()) +} + +#[inline] +async fn connect_with_path(ms_timeout: u64, path: &str) -> ResultType> { + let client = timeout(ms_timeout, Endpoint::connect(path)).await??; Ok(ConnectionTmpl::new(client)) } +#[cfg(target_os = "linux")] +pub async fn connect_for_uid( + ms_timeout: u64, + uid: u32, + postfix: &str, +) -> ResultType> { + let path = Config::ipc_path_for_uid(uid, postfix); + connect_with_path(ms_timeout, &path).await +} + #[cfg(target_os = "linux")] #[tokio::main(flavor = "current_thread")] pub async fn start_pa() { @@ -1039,54 +1291,6 @@ pub async fn start_pa() { } } -#[inline] -#[cfg(not(windows))] -fn get_pid_file(postfix: &str) -> String { - let path = Config::ipc_path(postfix); - format!("{}.pid", path) -} - -#[cfg(not(any(windows, target_os = "android", target_os = "ios")))] -async fn check_pid(postfix: &str) { - let pid_file = get_pid_file(postfix); - if let Ok(mut file) = File::open(&pid_file) { - let mut content = String::new(); - file.read_to_string(&mut content).ok(); - let pid = content.parse::().unwrap_or(0); - if pid > 0 { - use hbb_common::sysinfo::System; - let mut sys = System::new(); - sys.refresh_processes(); - if let Some(p) = sys.process(pid.into()) { - if let Some(current) = sys.process((std::process::id() as usize).into()) { - if current.name() == p.name() { - // double check with connect - if connect(1000, postfix).await.is_ok() { - return; - } - } - } - } - } - } - // if not remove old ipc file, the new ipc creation will fail - // if we remove a ipc file, but the old ipc process is still running, - // new connection to the ipc will connect to new ipc, old connection to old ipc still keep alive - std::fs::remove_file(&Config::ipc_path(postfix)).ok(); -} - -#[inline] -#[cfg(not(windows))] -fn write_pid(postfix: &str) { - let path = get_pid_file(postfix); - if let Ok(mut file) = File::create(&path) { - use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o0777)).ok(); - file.write_all(&std::process::id().to_string().into_bytes()) - .ok(); - } -} - pub struct ConnectionTmpl { inner: Framed, } @@ -1550,9 +1754,10 @@ pub fn close_all_instances() -> ResultType { } } +#[cfg(windows)] #[tokio::main(flavor = "current_thread")] pub async fn connect_to_user_session(usid: Option) -> ResultType<()> { - let mut stream = crate::ipc::connect(1000, crate::POSTFIX_SERVICE).await?; + let mut stream = crate::ipc::connect_service(1000).await?; timeout(1000, stream.send(&crate::ipc::Data::UserSid(usid))).await??; Ok(()) } @@ -1678,13 +1883,76 @@ pub async fn update_controlling_session_count(count: usize) -> ResultType<()> { #[cfg(target_os = "linux")] #[tokio::main(flavor = "current_thread")] pub async fn get_terminal_session_count() -> ResultType { - let ms_timeout = 1_000; - let mut c = connect(ms_timeout, "").await?; - c.send(&Data::TerminalSessionCount(0)).await?; - if let Some(Data::TerminalSessionCount(c)) = c.next_timeout(ms_timeout).await? { - return Ok(c); + let timeout_ms = 1_000; + let effective_uid = unsafe { hbb_common::libc::geteuid() as u32 }; + let candidate_uids = terminal_count_candidate_uids(effective_uid); + let mut last_err: Option = None; + for candidate_uid in candidate_uids { + let socket_path = Config::ipc_path_for_uid(candidate_uid, ""); + let connect_result = timeout(timeout_ms, Endpoint::connect(&socket_path)) + .await + .map_err(|err| { + anyhow::anyhow!( + "Timeout connecting to terminal ipc at {}: {}", + socket_path, + err + ) + }); + let connection = match connect_result { + Ok(Ok(connection)) => connection, + Ok(Err(err)) => { + last_err = Some(anyhow::anyhow!( + "Failed to connect to terminal ipc at {}: {}", + socket_path, + err + )); + continue; + } + Err(err) => { + last_err = Some(err); + continue; + } + }; + let mut ipc_conn = ConnectionTmpl::new(connection); + if let Err(err) = ipc_conn.send(&Data::TerminalSessionCount(0)).await { + last_err = Some(anyhow::anyhow!( + "Failed to request terminal session count via ipc at {}: {}", + socket_path, + err + )); + continue; + } + match ipc_conn.next_timeout(timeout_ms).await { + Ok(Some(Data::TerminalSessionCount(session_count))) => { + return Ok(session_count); + } + Ok(None) => { + last_err = Some(anyhow::anyhow!( + "Invalid response when requesting terminal session count via ipc at {}", + socket_path + )); + } + Ok(other) => { + last_err = Some(anyhow::anyhow!( + "Unexpected response when requesting terminal session count via ipc at {}: {:?}", + socket_path, + other.map(|v| std::mem::discriminant(&v)) + )); + } + Err(err) => { + last_err = Some(anyhow::anyhow!( + "Failed to read terminal session count via ipc at {}: {}", + socket_path, + err + )); + } + } + } + if let Some(err) = last_err { + Err(err.into()) + } else { + Ok(0) } - Ok(0) } async fn handle_wayland_screencast_restore_token( @@ -1715,9 +1983,30 @@ pub async fn set_install_option(k: String, v: String) -> ResultType<()> { #[cfg(test)] mod test { use super::*; + #[test] fn verify_ffi_enum_data_size() { println!("{}", std::mem::size_of::()); assert!(std::mem::size_of::() <= 120); } + + #[cfg(target_os = "linux")] + #[test] + fn test_ipc_path_differs_by_uid_for_cm() { + let effective_uid = unsafe { hbb_common::libc::geteuid() as u32 }; + let other_uid = effective_uid.saturating_add(1); + let postfix = "_cm"; + + // Default connect path targets the current effective uid. + assert_eq!( + Config::ipc_path(postfix), + Config::ipc_path_for_uid(effective_uid, postfix) + ); + // A different uid yields a different socket path - this is the root cause of the + // cross-user regression when root spawns a user process but still connects as uid 0. + assert_ne!( + Config::ipc_path(postfix), + Config::ipc_path_for_uid(other_uid, postfix) + ); + } } diff --git a/src/ipc/auth.rs b/src/ipc/auth.rs new file mode 100644 index 000000000..746a32eed --- /dev/null +++ b/src/ipc/auth.rs @@ -0,0 +1,1036 @@ +use crate::ipc::{Connection, ConnectionTmpl}; +#[cfg(all(windows, not(feature = "flutter")))] +use hbb_common::sha2::{Digest, Sha256}; +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] +use hbb_common::{anyhow, bail, log, ResultType}; +#[cfg(any(target_os = "linux", target_os = "macos"))] +use hbb_common::{ + libc, + tokio::io::{AsyncRead, AsyncWrite}, +}; +#[cfg(windows)] +use parity_tokio_ipc::SecurityAttributes; +#[cfg(windows)] +use std::io; +#[cfg(all(windows, not(feature = "flutter")))] +use std::io::Read; +#[cfg(target_os = "macos")] +use std::os::unix::fs::MetadataExt; +#[cfg(any(target_os = "linux", target_os = "macos"))] +use std::os::unix::io::RawFd; +#[cfg(windows)] +use std::os::windows::io::AsRawHandle; +#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] +use std::{ + fs, + path::{Path, PathBuf}, + sync::{Mutex, OnceLock}, +}; +#[cfg(windows)] +use windows::Win32::{Foundation::HANDLE, System::Pipes::GetNamedPipeClientProcessId}; + +#[cfg(windows)] +#[inline] +pub(crate) fn should_allow_everyone_create_on_windows(postfix: &str) -> bool { + postfix.is_empty() || hbb_common::config::is_service_ipc_postfix(postfix) +} + +#[cfg(windows)] +#[inline] +pub(crate) fn portable_service_listener_security_attributes() -> io::Result { + let user_sid = crate::platform::windows::current_process_user_sid_string().map_err(|err| { + io::Error::new( + io::ErrorKind::Other, + format!("failed to resolve current process SID: {}", err), + ) + })?; + debug_assert!( + user_sid.starts_with("S-1-") + && user_sid + .bytes() + .all(|byte| byte.is_ascii_digit() || byte == b'-'), + "current_process_user_sid_string returned a non-SDDL SID: {}", + user_sid + ); + // SDDL: + // - `D:P` => protected DACL (no inherited ACEs) + // - `(A;;GA;;;SY)` => allow GENERIC_ALL to LocalSystem + // - `(A;;GA;;;{user_sid})` => allow GENERIC_ALL to current process user SID + // References: + // - Security Descriptor String Format: https://learn.microsoft.com/en-us/windows/win32/secauthz/security-descriptor-string-format + // - ACE strings in SDDL: https://learn.microsoft.com/en-us/windows/win32/secauthz/ace-strings + let sddl = format!("D:P(A;;GA;;;SY)(A;;GA;;;{user_sid})"); + SecurityAttributes::from_sddl(&sddl).map_err(|err| { + io::Error::new( + io::ErrorKind::Other, + format!( + "failed to build portable service listener security attributes from SDDL '{}': {}", + sddl, err + ), + ) + }) +} + +#[cfg(target_os = "macos")] +#[inline] +fn macos_service_ipc_allows_gui_and_service_binaries( + peer_exe: &Path, + current_exe: &Path, + postfix: &str, +) -> bool { + if postfix != crate::POSTFIX_SERVICE { + return false; + } + let Some(peer_dir) = peer_exe.parent() else { + return false; + }; + let Some(current_dir) = current_exe.parent() else { + return false; + }; + if !executable_paths_match(peer_dir, current_dir) { + return false; + } + + // On installed macOS builds, `_service` is listened by the `service` binary while the GUI + // process connects from the app executable within the same app bundle. + let gui_exe_name = std::ffi::OsString::from(crate::get_app_name()); + let gui_exe = gui_exe_name.as_os_str(); + let service_exe = std::ffi::OsStr::new("service"); + let allowed_exe = [Some(gui_exe), Some(service_exe)]; + let peer_name = peer_exe.file_name(); + let current_name = current_exe.file_name(); + allowed_exe + .iter() + .any(|name| os_str_eq_ignore_ascii_case(peer_name, *name)) + && allowed_exe + .iter() + .any(|name| os_str_eq_ignore_ascii_case(current_name, *name)) +} + +#[cfg(target_os = "windows")] +#[inline] +fn windows_portable_service_ipc_allows_logon_helper_executable( + _peer_exe: &Path, + postfix: &str, +) -> bool { + if postfix != "_portable_service" { + return false; + } + #[cfg(feature = "flutter")] + { + false + } + #[cfg(not(feature = "flutter"))] + { + let Some((_, expected)) = crate::platform::windows::portable_service_logon_helper_paths() + else { + return false; + }; + let Ok(expected) = fs::canonicalize(expected) else { + return false; + }; + let Ok(current_exe) = current_exe_canonical_path() else { + return false; + }; + portable_service_helper_is_trusted(_peer_exe, &expected, ¤t_exe) + } +} + +#[cfg(windows)] +#[inline] +pub(crate) fn is_allowed_windows_session_scoped_peer( + client_is_system: bool, + client_session_id: Option, + expected_session_id: Option, +) -> bool { + client_is_system + || matches!( + (client_session_id, expected_session_id), + (Some(client), Some(expected)) if client == expected + ) +} + +#[cfg(windows)] +#[inline] +fn is_allowed_windows_portable_service_peer( + client_is_system: Option, + _client_session_id: Option, + _expected_session_id: Option, +) -> bool { + // Portable-service listener DACL includes SYSTEM and current-process SID. + // In the portable-service path, current process is expected to run as SYSTEM, + // and the higher-layer peer policy stays SYSTEM-only. + matches!(client_is_system, Some(true)) +} + +#[cfg(any(target_os = "macos", target_os = "linux"))] +#[inline] +pub(crate) fn is_allowed_service_peer_uid(peer_uid: u32, active_uid: Option) -> bool { + // Root is allowed at the UID gate because the service side may run as root. + // Callers still enforce executable matching before accepting service-scoped peers. + peer_uid == 0 || active_uid.is_some_and(|uid| uid == peer_uid) +} + +#[cfg(target_os = "macos")] +#[inline] +fn console_owner_uid() -> Option { + fs::metadata("/dev/console") + .ok() + .map(|metadata| metadata.uid()) +} + +#[cfg(target_os = "macos")] +#[inline] +fn active_uid_strict() -> Option { + // Prefer the filesystem metadata over parsing external command output. + console_owner_uid() +} + +#[cfg(target_os = "linux")] +#[inline] +fn active_uid_strict() -> Option { + let reported_uid_raw = crate::platform::linux::get_active_userid(); + let trimmed = reported_uid_raw.trim(); + if let Ok(uid) = trimmed.parse::() { + return Some(uid); + } + if trimmed.is_empty() { + log::debug!("Failed to resolve active user uid on linux: active uid is empty"); + } else { + log::warn!("Failed to parse active user uid on linux: '{}'", trimmed); + } + None +} + +#[cfg(any(target_os = "linux", target_os = "macos"))] +#[inline] +pub(crate) fn active_uid() -> Option { + active_uid_strict() +} + +#[cfg(any(target_os = "linux", target_os = "macos"))] +#[inline] +pub(crate) fn peer_uid_from_fd(fd: RawFd) -> Option { + #[cfg(target_os = "linux")] + { + return peer_cred_from_fd(fd).map(|cred| cred.uid as u32); + } + #[cfg(target_os = "macos")] + { + let mut uid = 0; + let mut gid = 0; + if unsafe { libc::getpeereid(fd, &mut uid, &mut gid) } == 0 { + Some(uid as u32) + } else { + None + } + } +} + +#[cfg(any(target_os = "linux", target_os = "macos"))] +#[inline] +fn peer_pid_from_fd(fd: RawFd) -> Option { + #[cfg(target_os = "linux")] + { + return peer_cred_from_fd(fd).and_then(|cred| (cred.pid > 0).then_some(cred.pid as u32)); + } + #[cfg(target_os = "macos")] + { + let mut pid = 0; + let mut len = std::mem::size_of::() as _; + let rc = unsafe { + libc::getsockopt( + fd, + libc::SOL_LOCAL, + libc::LOCAL_PEERPID, + &mut pid as *mut _ as *mut libc::c_void, + &mut len, + ) + }; + if rc == 0 && pid > 0 { + Some(pid as _) + } else { + None + } + } +} + +#[cfg(target_os = "linux")] +#[inline] +fn peer_cred_from_fd(fd: RawFd) -> Option { + let mut cred: libc::ucred = unsafe { std::mem::zeroed() }; + let mut len = std::mem::size_of::() as _; + let rc = unsafe { + libc::getsockopt( + fd, + libc::SOL_SOCKET, + libc::SO_PEERCRED, + &mut cred as *mut _ as *mut libc::c_void, + &mut len, + ) + }; + if rc == 0 { + Some(cred) + } else { + None + } +} + +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] +#[inline] +fn current_exe_canonical_path() -> ResultType { + let current = std::env::current_exe() + .map_err(|err| anyhow::anyhow!("Failed to resolve current executable path: {}", err))?; + fs::canonicalize(¤t).map_err(|err| { + anyhow::anyhow!( + "Failed to canonicalize current executable path '{}': {}", + current.display(), + err + ) + .into() + }) +} + +#[cfg(target_os = "linux")] +#[inline] +fn peer_exe_canonical_path_by_pid(peer_pid: u32) -> ResultType { + let proc_exe = PathBuf::from(format!("/proc/{peer_pid}/exe")); + let peer_exe = fs::read_link(&proc_exe).map_err(|err| { + anyhow::anyhow!( + "Failed to read peer executable link '{}': {}", + proc_exe.display(), + err + ) + })?; + fs::canonicalize(&peer_exe).map_err(|err| { + anyhow::anyhow!( + "Failed to canonicalize peer executable path '{}': {}", + peer_exe.display(), + err + ) + .into() + }) +} + +#[cfg(target_os = "macos")] +#[inline] +fn peer_exe_canonical_path_by_pid(peer_pid: u32) -> ResultType { + const PROC_PIDPATH_BUF_SIZE: usize = libc::PROC_PIDPATHINFO_MAXSIZE as _; + let mut buffer = vec![0u8; PROC_PIDPATH_BUF_SIZE]; + let length = unsafe { + libc::proc_pidpath( + peer_pid as _, + buffer.as_mut_ptr() as _, + PROC_PIDPATH_BUF_SIZE as _, + ) + }; + if length <= 0 { + bail!("Failed to query peer process path from pid {}", peer_pid); + } + buffer.truncate(length as _); + let path = PathBuf::from(String::from_utf8_lossy(&buffer).to_string()); + fs::canonicalize(&path).map_err(|err| { + anyhow::anyhow!( + "Failed to canonicalize peer executable path '{}': {}", + path.display(), + err + ) + .into() + }) +} + +#[cfg(target_os = "windows")] +#[inline] +fn peer_exe_canonical_path_by_pid(peer_pid: u32) -> ResultType { + let path = crate::platform::windows::get_process_executable_path(peer_pid)?; + fs::canonicalize(&path).map_err(|err| { + anyhow::anyhow!( + "Failed to canonicalize peer executable path '{}': {}", + path.display(), + err + ) + .into() + }) +} + +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] +#[inline] +pub(crate) fn executable_paths_match(left: &Path, right: &Path) -> bool { + #[cfg(target_os = "windows")] + { + // Callers pass paths resolved through fs::canonicalize() first, so NT + // namespace paths and 8.3 short names are expected to be resolved before + // this check. Keep this normalization limited to remaining Win32 spelling + // differences. + fn normalize(path: &Path) -> String { + let mut normalized = path.to_string_lossy().replace('/', "\\"); + if let Some(stripped) = normalized.strip_prefix(r"\\?\") { + normalized = stripped.to_owned(); + } + normalized.to_ascii_lowercase() + } + return normalize(left) == normalize(right); + } + #[cfg(target_os = "macos")] + { + return paths_refer_to_same_file(left, right); + } + #[cfg(not(any(target_os = "windows", target_os = "macos")))] + { + left == right + } +} + +#[cfg(target_os = "macos")] +#[inline] +fn paths_refer_to_same_file(left: &Path, right: &Path) -> bool { + if left == right { + return true; + } + let (Ok(left), Ok(right)) = (fs::metadata(left), fs::metadata(right)) else { + return false; + }; + left.dev() == right.dev() && left.ino() == right.ino() +} + +#[cfg(target_os = "macos")] +#[inline] +fn os_str_eq_ignore_ascii_case( + left: Option<&std::ffi::OsStr>, + right: Option<&std::ffi::OsStr>, +) -> bool { + let (Some(left), Some(right)) = (left, right) else { + return false; + }; + left.to_string_lossy() + .eq_ignore_ascii_case(&right.to_string_lossy()) +} + +#[cfg(all(windows, not(feature = "flutter")))] +#[inline] +fn file_sha256(path: &Path) -> ResultType<[u8; 32]> { + let mut file = fs::File::open(path)?; + let mut hasher = Sha256::new(); + let mut buffer = [0u8; 8 * 1024]; + loop { + let read_bytes = file.read(&mut buffer)?; + if read_bytes == 0 { + break; + } + hasher.update(&buffer[..read_bytes]); + } + Ok(hasher.finalize().into()) +} + +#[cfg(all(windows, not(feature = "flutter")))] +#[inline] +fn portable_service_helper_is_trusted( + peer_exe: &Path, + expected_exe: &Path, + current_exe: &Path, +) -> bool { + if !executable_paths_match(peer_exe, expected_exe) { + return false; + } + let peer_hash = match file_sha256(peer_exe) { + Ok(hash) => hash, + Err(err) => { + log::warn!( + "Failed to hash peer portable helper executable '{}': {}", + peer_exe.display(), + err + ); + return false; + } + }; + let current_hash = match file_sha256(current_exe) { + Ok(hash) => hash, + Err(err) => { + log::warn!( + "Failed to hash current executable '{}' for portable helper trust check: {}", + current_exe.display(), + err + ); + return false; + } + }; + peer_hash == current_hash +} + +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] +#[inline] +fn ensure_peer_executable_matches_current_by_pid(peer_pid: u32, postfix: &str) -> ResultType<()> { + let peer_exe = peer_exe_canonical_path_by_pid(peer_pid)?; + let current_exe = current_exe_canonical_path()?; + if executable_paths_match(&peer_exe, ¤t_exe) { + return Ok(()); + } + #[cfg(target_os = "macos")] + if macos_service_ipc_allows_gui_and_service_binaries(&peer_exe, ¤t_exe, postfix) { + return Ok(()); + } + #[cfg(target_os = "windows")] + if windows_portable_service_ipc_allows_logon_helper_executable(&peer_exe, postfix) { + return Ok(()); + } + bail!( + "Peer executable path mismatch on ipc channel '{}': peer_pid={}, peer_exe='{}', current_exe='{}'", + postfix, + peer_pid, + peer_exe.display(), + current_exe.display() + ); +} + +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] +#[inline] +pub(crate) fn ensure_peer_executable_matches_current_by_pid_opt( + peer_pid: Option, + postfix: &str, +) -> ResultType<()> { + let peer_pid = peer_pid.ok_or_else(|| { + anyhow::anyhow!("Failed to resolve peer pid on ipc channel '{}'", postfix) + })?; + ensure_peer_executable_matches_current_by_pid(peer_pid, postfix) +} + +#[cfg(target_os = "linux")] +#[inline] +pub(crate) fn ensure_peer_executable_matches_current_by_fd( + fd: RawFd, + postfix: &str, +) -> ResultType<()> { + let peer_pid = peer_pid_from_fd(fd).ok_or_else(|| { + anyhow::anyhow!("Failed to resolve peer pid on ipc channel '{}'", postfix) + })?; + ensure_peer_executable_matches_current_by_pid(peer_pid, postfix) +} + +#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] +const UNAUTHORIZED_IPC_LOG_INTERVAL: std::time::Duration = std::time::Duration::from_secs(5); + +#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] +#[derive(Default)] +struct UnauthorizedIpcLogThrottle { + last_log_at: Option, + suppressed: u64, +} + +#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] +impl UnauthorizedIpcLogThrottle { + #[inline] + fn on_reject(&mut self, now: std::time::Instant) -> Option { + if let Some(last) = self.last_log_at { + if now.saturating_duration_since(last) < UNAUTHORIZED_IPC_LOG_INTERVAL { + self.suppressed += 1; + return None; + } + } + self.last_log_at = Some(now); + Some(std::mem::take(&mut self.suppressed)) + } +} + +#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] +#[inline] +fn throttled_unauthorized_ipc_log( + throttle_cell: &OnceLock>, + emit: impl FnOnce(u64), +) { + let throttle = throttle_cell.get_or_init(|| Mutex::new(UnauthorizedIpcLogThrottle::default())); + let should_log = match throttle.lock() { + Ok(mut throttle) => throttle.on_reject(std::time::Instant::now()), + Err(_) => Some(0), + }; + if let Some(suppressed) = should_log { + emit(suppressed); + } +} + +#[cfg(any(target_os = "linux", target_os = "macos"))] +#[inline] +fn log_rejected_service_connection(postfix: &str, peer_uid: Option, active_uid: Option) { + static LOG_THROTTLE: OnceLock> = OnceLock::new(); + throttled_unauthorized_ipc_log(&LOG_THROTTLE, |suppressed| { + if suppressed > 0 { + log::warn!( + "Rejected unauthorized connection on protected service-scoped IPC channel: postfix={}, peer_uid={:?}, active_uid={:?} (suppressed {} similar events)", + postfix, + peer_uid, + active_uid, + suppressed + ); + } else { + log::warn!( + "Rejected unauthorized connection on protected service-scoped IPC channel: postfix={}, peer_uid={:?}, active_uid={:?}", + postfix, + peer_uid, + active_uid + ); + } + }); +} + +#[cfg(target_os = "linux")] +#[inline] +pub(crate) fn log_rejected_uinput_connection( + postfix: &str, + peer_uid: Option, + active_uid: Option, +) { + static LOG_THROTTLE: OnceLock> = OnceLock::new(); + throttled_unauthorized_ipc_log(&LOG_THROTTLE, |suppressed| { + if suppressed > 0 { + log::warn!( + "Rejected unauthorized connection on uinput ipc channel: postfix={}, peer_uid={:?}, active_uid={:?} (suppressed {} similar events)", + postfix, + peer_uid, + active_uid, + suppressed + ); + } else { + log::warn!( + "Rejected unauthorized connection on uinput ipc channel: postfix={}, peer_uid={:?}, active_uid={:?}", + postfix, + peer_uid, + active_uid + ); + } + }); +} + +#[cfg(windows)] +#[inline] +pub(crate) fn log_rejected_windows_ipc_connection( + postfix: &str, + peer_pid: Option, + peer_session_id: Option, + expected_session_id: Option, + peer_is_system: Option, +) { + static LOG_THROTTLE: OnceLock> = OnceLock::new(); + throttled_unauthorized_ipc_log(&LOG_THROTTLE, |suppressed| { + if suppressed > 0 { + log::warn!( + "Rejected unauthorized connection on ipc channel: postfix={}, peer_pid={:?}, peer_session_id={:?}, expected_session_id={:?}, peer_is_system={:?} (suppressed {} similar events)", + postfix, + peer_pid, + peer_session_id, + expected_session_id, + peer_is_system, + suppressed + ); + } else { + log::warn!( + "Rejected unauthorized connection on ipc channel: postfix={}, peer_pid={:?}, peer_session_id={:?}, expected_session_id={:?}, peer_is_system={:?}", + postfix, + peer_pid, + peer_session_id, + expected_session_id, + peer_is_system + ); + } + }); +} + +#[cfg(any(target_os = "linux", target_os = "macos"))] +pub(crate) fn authorize_service_scoped_ipc_connection(stream: &Connection, postfix: &str) -> bool { + let peer_pid = stream.peer_pid(); + let (authorized, peer_uid, active_uid) = stream.service_authorization_status(); + if !authorized { + log_rejected_service_connection(postfix, peer_uid, active_uid); + return false; + } + if let Err(err) = ensure_peer_executable_matches_current_by_pid_opt(peer_pid, postfix) { + log::warn!( + "Rejected unauthorized connection on protected service-scoped IPC channel due to executable mismatch: postfix={}, peer_pid={:?}, err={}", + postfix, + peer_pid, + err + ); + return false; + } + true +} + +#[cfg(windows)] +pub(crate) fn authorize_windows_main_ipc_connection(stream: &Connection, postfix: &str) -> bool { + let (authorized, peer_pid, peer_session_id, server_session_id, peer_is_system) = + stream.server_authorization_status(); + if !authorized { + log_rejected_windows_ipc_connection( + postfix, + peer_pid, + peer_session_id, + server_session_id, + peer_is_system, + ); + return false; + } + if let Err(err) = ensure_peer_executable_matches_current_by_pid_opt(peer_pid, postfix) { + log::warn!( + "Rejected unauthorized connection on ipc channel due to executable mismatch: postfix={}, peer_pid={:?}, err={}", + postfix, + peer_pid, + err + ); + return false; + } + true +} + +#[cfg(windows)] +pub(crate) fn authorize_windows_portable_service_ipc_connection( + stream: &Connection, + postfix: &str, +) -> bool { + // Portable service IPC policy: + // - only SYSTEM peers are authorized by is_allowed_windows_portable_service_peer() + // - expected_session_id is still collected for diagnostics and identity checks + // - final privilege boundary is enforced by named-pipe ACL + one-time token handshake + // - when peer identity is unavailable on some hosts, executable verification remains + // best-effort telemetry (not fail-closed) to avoid breaking valid SYSTEM bootstrap + // flows that cannot be fully introspected + let expected_session_id = crate::platform::windows::get_current_process_session_id(); + let (authorized, peer_pid, peer_session_id, peer_is_system) = + stream.portable_service_authorization_status_for_session(expected_session_id); + if !authorized { + // Session lookup may succeed while SYSTEM identity lookup fails, so only the + // SYSTEM identity result determines whether peer identity is unavailable here. + // Don't use `peer_pid.is_some() && peer_session_id.is_none() && peer_is_system.is_none();` here. + let identity_unavailable = peer_pid.is_some() && peer_is_system.is_none(); + if identity_unavailable { + // In portable-service startup, resolving SYSTEM peer identity may fail on some hosts. + // `ProcessIdToSessionId` can still succeed while `OpenProcessToken(TOKEN_QUERY)` is + // denied by the peer token DACL or missing privileges. Treat that partial identity + // failure as unavailable and defer final authorization to pipe ACL + token handshake. + if let Err(err) = ensure_peer_executable_matches_current_by_pid_opt(peer_pid, postfix) { + log::warn!( + "Portable service ipc peer identity unavailable and executable verification failed; continue with ACL+token-gated flow: postfix={}, peer_pid={:?}, err={}", + postfix, + peer_pid, + err + ); + } else { + log::warn!( + "Portable service ipc peer identity unavailable; executable verification matched, continue with ACL+token-gated flow: postfix={}, peer_pid={:?}, expected_session_id={:?}", + postfix, + peer_pid, + expected_session_id + ); + } + return true; + } + log::warn!( + "Rejected unauthorized connection on portable service ipc channel: postfix={}, peer_pid={:?}, peer_session_id={:?}, expected_session_id={:?}, peer_is_system={:?}", + postfix, + peer_pid, + peer_session_id, + expected_session_id, + peer_is_system + ); + return false; + } + true +} + +#[cfg(any(target_os = "linux", target_os = "macos"))] +impl ConnectionTmpl +where + T: AsyncRead + AsyncWrite + std::marker::Unpin + std::os::unix::io::AsRawFd, +{ + pub(super) fn peer_uid(&self) -> Option { + peer_uid_from_fd(self.inner.get_ref().as_raw_fd()) + } + + fn service_authorization_status(&self) -> (bool, Option, Option) { + let peer_uid = self.peer_uid(); + // On Linux, `_service` can use the cached active UID from the service loop for + // stable config sync. Uinput does a fresh active-UID lookup in its own authorizer. + let active_uid = active_uid(); + let authorized = peer_uid.is_some_and(|uid| is_allowed_service_peer_uid(uid, active_uid)); + (authorized, peer_uid, active_uid) + } + + pub(super) fn peer_pid(&self) -> Option { + peer_pid_from_fd(self.inner.get_ref().as_raw_fd()) + } +} + +#[cfg(windows)] +impl ConnectionTmpl { + fn peer_pid(&self) -> Option { + let pipe_handle = self.inner.get_ref().as_raw_handle(); + if pipe_handle.is_null() { + return None; + } + let mut pid = 0u32; + let ok = unsafe { GetNamedPipeClientProcessId(HANDLE(pipe_handle), &mut pid as *mut u32) } + .is_ok(); + if ok && pid != 0 { + Some(pid) + } else { + None + } + } + + fn server_authorization_status( + &self, + ) -> (bool, Option, Option, Option, Option) { + let peer_pid = self.peer_pid(); + let server_session_id = crate::platform::windows::get_current_process_session_id(); + let peer_session_id = + peer_pid.and_then(crate::platform::windows::get_session_id_of_process); + let peer_is_system_result = + peer_pid.map(crate::platform::windows::is_process_running_as_system); + let peer_is_system = peer_is_system_result + .as_ref() + .and_then(|r| r.as_ref().ok().copied()); + if server_session_id.is_none() && !peer_is_system.unwrap_or(false) { + // When the server session id cannot be determined, the session-id allow-path is + // disabled and only SYSTEM peers can be authorized. + log::debug!( + "IPC authorization: server session id unavailable; rejecting non-SYSTEM peer, peer_pid={:?}, peer_session_id={:?}", + peer_pid, + peer_session_id + ); + } + let authorized = is_allowed_windows_session_scoped_peer( + peer_is_system.unwrap_or(false), + peer_session_id, + server_session_id, + ); + if !authorized { + if let (Some(pid), Some(Err(err))) = (peer_pid, peer_is_system_result.as_ref()) { + log::debug!( + "Failed to determine whether peer process is SYSTEM, pid={}, err={}", + pid, + err + ); + } + } + ( + authorized, + peer_pid, + peer_session_id, + server_session_id, + peer_is_system, + ) + } + + pub(crate) fn service_authorization_status_for_session( + &self, + expected_active_session_id: Option, + ) -> (bool, Option, Option, Option) { + let peer_pid = self.peer_pid(); + let peer_session_id = + peer_pid.and_then(crate::platform::windows::get_session_id_of_process); + let peer_is_system_result = + peer_pid.map(crate::platform::windows::is_process_running_as_system); + let peer_is_system = peer_is_system_result + .as_ref() + .and_then(|r| r.as_ref().ok().copied()); + let authorized = is_allowed_windows_session_scoped_peer( + peer_is_system.unwrap_or(false), + peer_session_id, + expected_active_session_id, + ); + if !authorized { + if let (Some(pid), Some(Err(err))) = (peer_pid, peer_is_system_result.as_ref()) { + log::debug!( + "Failed to determine whether peer process is SYSTEM, pid={}, err={}", + pid, + err + ); + } + } + (authorized, peer_pid, peer_session_id, peer_is_system) + } + + pub(crate) fn portable_service_authorization_status_for_session( + &self, + expected_active_session_id: Option, + ) -> (bool, Option, Option, Option) { + // Portable-service policy: + // only SYSTEM peers are allowed. + let (_service_authorized, peer_pid, peer_session_id, peer_is_system) = + self.service_authorization_status_for_session(expected_active_session_id); + ( + is_allowed_windows_portable_service_peer( + peer_is_system, + peer_session_id, + expected_active_session_id, + ), + peer_pid, + peer_session_id, + peer_is_system, + ) + } +} + +#[cfg(test)] +mod tests { + #[test] + #[cfg(any(target_os = "macos", target_os = "linux"))] + fn test_service_peer_uid_policy() { + assert!(super::is_allowed_service_peer_uid(0, None)); + assert!(super::is_allowed_service_peer_uid(501, Some(501))); + assert!(!super::is_allowed_service_peer_uid(502, Some(501))); + assert!(!super::is_allowed_service_peer_uid(501, None)); + } + + #[test] + #[cfg(windows)] + fn test_windows_server_peer_policy() { + assert!(super::is_allowed_windows_session_scoped_peer( + true, None, None + )); + assert!(super::is_allowed_windows_session_scoped_peer( + false, + Some(1), + Some(1) + )); + assert!(!super::is_allowed_windows_session_scoped_peer( + false, + Some(1), + Some(2) + )); + assert!(!super::is_allowed_windows_session_scoped_peer( + false, + None, + Some(1) + )); + } + + #[test] + #[cfg(windows)] + fn test_windows_portable_service_peer_policy() { + assert!(super::is_allowed_windows_portable_service_peer( + Some(true), + None, + None + )); + assert!(!super::is_allowed_windows_portable_service_peer( + Some(false), + Some(1), + Some(1) + )); + assert!(!super::is_allowed_windows_portable_service_peer( + Some(false), + Some(1), + Some(2) + )); + assert!(!super::is_allowed_windows_portable_service_peer( + None, + Some(1), + Some(1) + )); + } + + #[test] + #[cfg(windows)] + fn test_should_allow_everyone_create_on_windows_policy() { + assert!(super::should_allow_everyone_create_on_windows("")); + assert!(super::should_allow_everyone_create_on_windows("_service")); + assert!(!super::should_allow_everyone_create_on_windows( + "_portable_service" + )); + } + + #[test] + #[cfg(windows)] + fn test_executable_paths_match_windows_normalization() { + let left = std::path::PathBuf::from(r"\\?\C:\Program Files\RustDesk\RustDesk.exe"); + let right = std::path::PathBuf::from(r"c:\program files\rustdesk\rustdesk.exe"); + assert!(super::executable_paths_match(&left, &right)); + } + + #[test] + #[cfg(target_os = "macos")] + fn test_os_str_eq_ignore_ascii_case_for_process_names() { + assert!(super::os_str_eq_ignore_ascii_case( + Some(std::ffi::OsStr::new("RustDesk")), + Some(std::ffi::OsStr::new("rustdesk")) + )); + assert!(!super::os_str_eq_ignore_ascii_case( + Some(std::ffi::OsStr::new("RustDesk")), + Some(std::ffi::OsStr::new("service")) + )); + } + + #[cfg(all(windows, not(feature = "flutter")))] + struct TempDirGuard(std::path::PathBuf); + + #[cfg(all(windows, not(feature = "flutter")))] + impl Drop for TempDirGuard { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } + } + + #[test] + #[cfg(all(windows, not(feature = "flutter")))] + fn test_portable_service_helper_trust_requires_content_match() { + let unique = format!( + "rustdesk-portable-helper-trust-test-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() + ); + let base = std::env::temp_dir().join(unique); + std::fs::create_dir_all(&base).unwrap(); + let _cleanup = TempDirGuard(base.clone()); + + let current_exe = base.join("current.exe"); + let helper_exe = base.join("helper.exe"); + std::fs::write(¤t_exe, b"trusted-binary").unwrap(); + std::fs::write(&helper_exe, b"tampered-binary").unwrap(); + + assert!( + !super::portable_service_helper_is_trusted(&helper_exe, &helper_exe, ¤t_exe), + "helper trust check must reject path-match-only binaries with mismatched content" + ); + } + + #[test] + #[cfg(all(windows, not(feature = "flutter")))] + fn test_portable_service_helper_trust_accepts_matching_content() { + let unique = format!( + "rustdesk-portable-helper-trust-match-test-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() + ); + let base = std::env::temp_dir().join(unique); + std::fs::create_dir_all(&base).unwrap(); + let _cleanup = TempDirGuard(base.clone()); + + let current_exe = base.join("current.exe"); + let helper_exe = base.join("helper.exe"); + std::fs::write(¤t_exe, b"trusted-binary").unwrap(); + std::fs::write(&helper_exe, b"trusted-binary").unwrap(); + + assert!(super::portable_service_helper_is_trusted( + &helper_exe, + &helper_exe, + ¤t_exe + )); + } + + #[cfg(target_os = "macos")] + #[test] + fn test_console_owner_uid_matches_get_active_userid() { + let console_uid = + super::console_owner_uid().expect("/dev/console must have a resolvable uid"); + let raw_uid = crate::platform::macos::get_active_userid(); + let parsed_uid: u32 = raw_uid + .trim() + .parse() + .unwrap_or_else(|_| panic!("failed to parse get_active_userid() output: '{raw_uid}'")); + assert_eq!(parsed_uid, console_uid); + } +} diff --git a/src/ipc/fs.rs b/src/ipc/fs.rs new file mode 100644 index 000000000..e0157f3a9 --- /dev/null +++ b/src/ipc/fs.rs @@ -0,0 +1,951 @@ +#[cfg(target_os = "linux")] +use super::ipc_auth::active_uid; +use crate::ipc::{connect, Data}; +use hbb_common::{config, log, ResultType}; +use std::{ + ffi::CString, + io::{Error, ErrorKind}, + os::unix::ffi::OsStrExt, + path::Path, +}; + +struct FdGuard(i32); +impl Drop for FdGuard { + fn drop(&mut self) { + unsafe { + hbb_common::libc::close(self.0); + } + } +} + +#[cfg(target_os = "linux")] +#[inline] +pub(crate) fn terminal_count_candidate_uids(effective_uid: u32) -> Vec { + if effective_uid != 0 { + return vec![effective_uid]; + } + let mut candidates = Vec::with_capacity(2); + if let Some(uid) = active_uid().filter(|uid| *uid != 0) { + candidates.push(uid); + } + candidates.push(0); + candidates +} + +#[inline] +fn expected_ipc_parent_mode(postfix: &str) -> u32 { + if config::is_service_ipc_postfix(postfix) { + 0o0711 + } else { + 0o0700 + } +} + +fn open_ipc_parent_dir_fd(parent_c: &CString) -> std::io::Result { + let fd = unsafe { + hbb_common::libc::open( + parent_c.as_ptr(), + hbb_common::libc::O_RDONLY + | hbb_common::libc::O_DIRECTORY + | hbb_common::libc::O_CLOEXEC + | hbb_common::libc::O_NOFOLLOW, + ) + }; + if fd < 0 { + Err(std::io::Error::last_os_error()) + } else { + Ok(fd) + } +} + +// Remove one preexisting IPC artifact via an already-opened parent directory FD. +// +// Security intent: +// - Bind cleanup to the exact parent inode that passed O_NOFOLLOW + fstat checks. +// - Avoid path-based TOCTOU during scrub (e.g., parent path rename/swap race). +// +// Flow: +// 1) fstatat(..., AT_SYMLINK_NOFOLLOW) to inspect the target entry under parent_fd. +// 2) Decide file vs directory from st_mode. +// 3) unlinkat relative to parent_fd (AT_REMOVEDIR for directories). +// +// Error policy: +// - NotFound is treated as benign (already removed / raced away). +// - Other errors are surfaced explicitly. +fn remove_parent_entry_via_fd( + parent_fd: i32, + parent_dir: &Path, + entry_name: &str, +) -> ResultType<()> { + if entry_name.contains('/') { + return Err(Error::new( + ErrorKind::InvalidInput, + format!( + "invalid ipc parent entry name (contains '/'): parent={}, entry={}", + parent_dir.display(), + entry_name + ), + ) + .into()); + } + let entry_c = CString::new(entry_name.as_bytes().to_vec()).map_err(|err| { + Error::new( + ErrorKind::InvalidInput, + format!( + "invalid ipc parent entry name: parent={}, entry={}, err={}", + parent_dir.display(), + entry_name, + err + ), + ) + })?; + let mut stat: hbb_common::libc::stat = unsafe { std::mem::zeroed() }; + let stat_rc = unsafe { + hbb_common::libc::fstatat( + parent_fd, + entry_c.as_ptr(), + &mut stat, + hbb_common::libc::AT_SYMLINK_NOFOLLOW, + ) + }; + if stat_rc != 0 { + let err = std::io::Error::last_os_error(); + if err.kind() == ErrorKind::NotFound { + return Ok(()); + } + return Err(Error::new( + err.kind(), + format!( + "failed to stat preexisting ipc parent dir entry by fd: parent={}, entry={}, err={}", + parent_dir.display(), + entry_name, + err + ), + ) + .into()); + } + + let is_dir = (stat.st_mode & (hbb_common::libc::S_IFMT as hbb_common::libc::mode_t)) + == hbb_common::libc::S_IFDIR; + let unlink_flags = if is_dir { + hbb_common::libc::AT_REMOVEDIR + } else { + 0 + }; + let unlink_rc = + unsafe { hbb_common::libc::unlinkat(parent_fd, entry_c.as_ptr(), unlink_flags) }; + if unlink_rc != 0 { + let err = std::io::Error::last_os_error(); + if err.kind() == ErrorKind::NotFound { + return Ok(()); + } + return Err(Error::new( + err.kind(), + format!( + "failed to remove preexisting ipc parent dir entry by fd: parent={}, entry={}, err={}", + parent_dir.display(), + entry_name, + err + ), + ) + .into()); + } + Ok(()) +} + +fn scrub_preexisting_ipc_parent_entries( + parent_fd: i32, + parent_dir: &Path, + postfix: &str, +) -> ResultType<()> { + let ipc_basename = format!("ipc{}", postfix); + remove_parent_entry_via_fd(parent_fd, parent_dir, &ipc_basename)?; + remove_parent_entry_via_fd(parent_fd, parent_dir, &format!("{}.pid", ipc_basename))?; + Ok(()) +} + +fn remove_ipc_socket_via_secure_parent_fd(postfix: &str) -> ResultType<()> { + let path = config::Config::ipc_path(postfix); + let parent_dir = Path::new(&path) + .parent() + .ok_or_else(|| Error::new(ErrorKind::InvalidInput, format!("invalid ipc path: {path}")))?; + let parent_c = CString::new(parent_dir.as_os_str().as_bytes().to_vec())?; + let fd = match open_ipc_parent_dir_fd(&parent_c) { + Ok(fd) => fd, + Err(open_err) => { + if open_err.kind() == ErrorKind::NotFound { + return Ok(()); + } + return Err(Error::new( + open_err.kind(), + format!( + "failed to open ipc parent dir for stale socket cleanup (no-follow): postfix={}, parent={}, err={}", + postfix, + parent_dir.display(), + open_err + ), + ) + .into()); + } + }; + let _fd_guard = FdGuard(fd); + remove_parent_entry_via_fd(fd, parent_dir, &format!("ipc{}", postfix)) +} + +// Purpose: +// - Harden the IPC parent directory before creating/listening socket files. +// - Prevent symlink/path-race abuse and reject unsafe owner/mode. +// +// Approach: +// - Open parent dir with O_NOFOLLOW/O_DIRECTORY and operate on that fd. +// - Validate inode type/owner/mode via fstat. +// - For protected service postfix, optionally adopt owner (root only), then scrub stale +// rustdesk IPC artifacts when directory trust boundary changed. +// +// Main steps: +// 1) Resolve parent path and open/create directory securely. +// 2) Verify directory inode type and owner uid. +// 3) Enforce expected mode via fchmod on opened fd. +// 4) Scrub stale IPC artifacts when owner/mode was unsafe before hardening. +// +// References: +// - open(2): O_NOFOLLOW/O_DIRECTORY/O_CLOEXEC +// https://man7.org/linux/man-pages/man2/open.2.html +// - fstat(2): verify file type/metadata on opened fd +// https://man7.org/linux/man-pages/man2/fstat.2.html +// - fchown(2): adopt ownership when running as root +// https://man7.org/linux/man-pages/man2/chown.2.html +// - fchmod(2): enforce exact mode on opened fd +// https://man7.org/linux/man-pages/man2/fchmod.2.html +pub(crate) fn ensure_secure_ipc_parent_dir(path: &str, postfix: &str) -> ResultType { + let parent_dir = Path::new(path) + .parent() + .ok_or_else(|| Error::new(ErrorKind::InvalidInput, format!("invalid ipc path: {path}")))?; + // Harden against common TOCTOU by opening the parent directory with O_NOFOLLOW (so the parent + // itself cannot be a symlink) and then operating on its FD (fstat/fchown/fchmod). This ensures + // we mutate the inode we opened, though it does not protect against symlinks in ancestor path + // components. + let parent_c = CString::new(parent_dir.as_os_str().as_bytes().to_vec())?; + let fd = match open_ipc_parent_dir_fd(&parent_c) { + Ok(fd) => fd, + Err(open_err) => { + // If the directory doesn't exist yet, create it with the expected mode. The parent + // dir is intended to be a single-level /tmp path, so mkdir is sufficient here. + if open_err.raw_os_error() == Some(hbb_common::libc::ENOENT) { + let expected_mode = expected_ipc_parent_mode(postfix); + let rc = unsafe { + hbb_common::libc::mkdir( + parent_c.as_ptr(), + expected_mode as hbb_common::libc::mode_t, + ) + }; + if rc != 0 { + let mkdir_err = std::io::Error::last_os_error(); + // Handle a race where another process created the directory first. + if mkdir_err.raw_os_error() != Some(hbb_common::libc::EEXIST) { + return Err(Error::new( + mkdir_err.kind(), + format!( + "failed to mkdir ipc parent dir: postfix={}, parent={}, err={}", + postfix, + parent_dir.display(), + mkdir_err + ), + ) + .into()); + } + } + match open_ipc_parent_dir_fd(&parent_c) { + Ok(fd) => fd, + Err(err) => { + return Err(Error::new( + err.kind(), + format!( + "failed to open ipc parent dir (no-follow): postfix={}, parent={}, err={}", + postfix, + parent_dir.display(), + err + ), + ) + .into()); + } + } + } else { + return Err(Error::new( + open_err.kind(), + format!( + "failed to open ipc parent dir (no-follow): postfix={}, parent={}, err={}", + postfix, + parent_dir.display(), + open_err + ), + ) + .into()); + } + } + }; + let _fd_guard = FdGuard(fd); + + let mut st: hbb_common::libc::stat = unsafe { std::mem::zeroed() }; + if unsafe { hbb_common::libc::fstat(fd, &mut st as *mut _) } != 0 { + let os_err = std::io::Error::last_os_error(); + return Err(Error::new( + os_err.kind(), + format!( + "failed to stat ipc parent dir: postfix={}, parent={}, err={}", + postfix, + parent_dir.display(), + os_err + ), + ) + .into()); + } + let mode = st.st_mode as u32; + let is_dir = (mode & (hbb_common::libc::S_IFMT as u32)) == (hbb_common::libc::S_IFDIR as u32); + if !is_dir { + return Err(Error::new( + ErrorKind::PermissionDenied, + format!( + "ipc parent is not directory: postfix={}, parent={}", + postfix, + parent_dir.display() + ), + ) + .into()); + } + + let expected_uid = unsafe { hbb_common::libc::geteuid() as u32 }; + let mut owner_uid = st.st_uid as u32; + let mut adopted_foreign_service_parent = false; + // Service-scoped IPC may be created by different privilege contexts historically. + // If running as root on protected service postfix, try adopting ownership first. + if owner_uid != expected_uid && expected_uid == 0 && config::is_service_ipc_postfix(postfix) { + let rc = unsafe { + hbb_common::libc::fchown( + fd, + expected_uid as hbb_common::libc::uid_t, + hbb_common::libc::gid_t::MAX, + ) + }; + if rc == 0 { + let mut st2: hbb_common::libc::stat = unsafe { std::mem::zeroed() }; + if unsafe { hbb_common::libc::fstat(fd, &mut st2 as *mut _) } == 0 { + owner_uid = st2.st_uid as u32; + st = st2; + adopted_foreign_service_parent = true; + } + } else { + // Keep behavior unchanged; capture errno to ease diagnosing why chown failed. + let err = std::io::Error::last_os_error(); + log::warn!( + "Failed to chown ipc parent dir, parent={}, postfix={}, expected_uid={}, rc={}, err={:?}", + parent_dir.display(), + postfix, + expected_uid, + rc, + err + ); + } + } + if owner_uid != expected_uid { + return Err(Error::new( + ErrorKind::PermissionDenied, + format!( + "unsafe ipc parent owner, postfix={}, expected uid {expected_uid}, got {owner_uid}: {}", + postfix, + parent_dir.display() + ), + ) + .into()); + } + + let expected_mode = expected_ipc_parent_mode(postfix); + // Include special bits (setuid/setgid/sticky) to ensure the directory is hardened to the exact + // expected mode. + let current_mode = (st.st_mode as u32) & 0o7777; + let repaired_parent_mode = current_mode != expected_mode; + let had_untrusted_parent_mode = (current_mode & 0o022) != 0; + if repaired_parent_mode { + // Use fchmod on the opened fd to avoid path-race between check and chmod. + if unsafe { hbb_common::libc::fchmod(fd, expected_mode as hbb_common::libc::mode_t) } != 0 { + let os_err = std::io::Error::last_os_error(); + return Err(Error::new( + os_err.kind(), + format!( + "failed to chmod ipc parent dir: postfix={}, parent={}, err={}", + postfix, + parent_dir.display(), + os_err + ), + ) + .into()); + } + } + let should_scrub = + repaired_parent_mode || adopted_foreign_service_parent || had_untrusted_parent_mode; + Ok(should_scrub) +} + +pub(crate) fn scrub_secure_ipc_parent_dir(path: &str, postfix: &str) -> ResultType<()> { + let parent_dir = Path::new(path) + .parent() + .ok_or_else(|| Error::new(ErrorKind::InvalidInput, format!("invalid ipc path: {path}")))?; + let parent_c = CString::new(parent_dir.as_os_str().as_bytes().to_vec())?; + let fd = open_ipc_parent_dir_fd(&parent_c).map_err(|err| { + Error::new( + err.kind(), + format!( + "failed to open ipc parent dir for scrub (no-follow): postfix={}, parent={}, err={}", + postfix, + parent_dir.display(), + err + ), + ) + })?; + let _fd_guard = FdGuard(fd); + scrub_preexisting_ipc_parent_entries(fd, parent_dir, postfix) +} + +#[inline] +pub(crate) fn get_pid_file(postfix: &str) -> String { + let path = config::Config::ipc_path(postfix); + format!("{}.pid", path) +} + +// Purpose: +// - Write current process pid to pid file without following attacker-controlled symlinks. +// - Ensure the pid file is a regular file owned by the opened inode path. +// +// Approach: +// - Use libc open/fstat/write syscalls (FFI) so flags and inode validation are explicit. +// - Open file with O_NOFOLLOW/O_CLOEXEC and verify S_IFREG with fstat before write. +// - Keep unsafe scopes minimal and check syscall return values immediately. +// +// Main steps: +// 1) Secure-open pid file (without truncation). +// 2) Validate opened inode is a regular file owned by current euid. +// 3) Enforce pid file mode to 0600 and truncate via ftruncate after validation. +// 4) Write process id bytes through fd. +// +// Why not plain std::fs::write? +// - std::fs helpers cannot enforce this exact open-time hardening sequence +// (especially "open with O_NOFOLLOW, then fstat the same opened inode"). +// +// References: +// - open(2): O_NOFOLLOW/O_CLOEXEC/O_NONBLOCK +// https://man7.org/linux/man-pages/man2/open.2.html +// - fstat(2): verify file type on opened fd +// https://man7.org/linux/man-pages/man2/fstat.2.html +// - fchmod(2): enforce secure mode on reused pid file +// https://man7.org/linux/man-pages/man2/fchmod.2.html +// - ftruncate(2): truncate after validation +// https://man7.org/linux/man-pages/man2/ftruncate.2.html +// - write(2): write bytes via fd +// https://man7.org/linux/man-pages/man2/write.2.html +fn write_pid_file(path: &Path) -> ResultType<()> { + let path_c = CString::new(path.as_os_str().as_bytes().to_vec()).map_err(|err| { + Error::new( + ErrorKind::InvalidInput, + format!("invalid pid file path '{}': {}", path.display(), err), + ) + })?; + let flags = hbb_common::libc::O_WRONLY + | hbb_common::libc::O_CREAT + | hbb_common::libc::O_CLOEXEC + | hbb_common::libc::O_NOFOLLOW + | hbb_common::libc::O_NONBLOCK; + let fd = unsafe { hbb_common::libc::open(path_c.as_ptr(), flags, 0o0600) }; + if fd < 0 { + let os_err = std::io::Error::last_os_error(); + return Err(Error::new( + os_err.kind(), + format!( + "failed to open pid file with no-follow '{}': {}", + path.display(), + os_err + ), + ) + .into()); + } + let _fd_guard = FdGuard(fd); + let mut stat: hbb_common::libc::stat = unsafe { std::mem::zeroed() }; + if unsafe { hbb_common::libc::fstat(fd, &mut stat) } != 0 { + let os_err = std::io::Error::last_os_error(); + return Err(Error::new( + os_err.kind(), + format!("failed to stat pid file '{}': {}", path.display(), os_err), + ) + .into()); + } + if (stat.st_mode & (hbb_common::libc::S_IFMT as hbb_common::libc::mode_t)) + != (hbb_common::libc::S_IFREG as hbb_common::libc::mode_t) + { + return Err(Error::new( + ErrorKind::PermissionDenied, + format!("pid file path is not a regular file: '{}'", path.display()), + ) + .into()); + } + let expected_uid = unsafe { hbb_common::libc::geteuid() as u32 }; + if stat.st_uid as u32 != expected_uid { + return Err(Error::new( + ErrorKind::PermissionDenied, + format!( + "pid file owner mismatch: expected uid {}, got {} for '{}'", + expected_uid, + stat.st_uid, + path.display() + ), + ) + .into()); + } + if unsafe { hbb_common::libc::fchmod(fd, 0o600) } != 0 { + let os_err = std::io::Error::last_os_error(); + return Err(Error::new( + os_err.kind(), + format!("failed to chmod pid file '{}': {}", path.display(), os_err), + ) + .into()); + } + if unsafe { hbb_common::libc::ftruncate(fd, 0) } != 0 { + let os_err = std::io::Error::last_os_error(); + return Err(Error::new( + os_err.kind(), + format!( + "failed to truncate pid file '{}': {}", + path.display(), + os_err + ), + ) + .into()); + } + + let bytes = std::process::id().to_string(); + let buf = bytes.as_bytes(); + // `write(2)` is allowed to return a short write even for regular files. + // PID content is tiny and usually written in one shot, but we still loop + // until all bytes are persisted so this path is semantically correct. + let mut written = 0usize; + while written < buf.len() { + let rc = unsafe { + hbb_common::libc::write( + fd, + buf[written..].as_ptr() as *const hbb_common::libc::c_void, + buf.len() - written, + ) + }; + if rc < 0 { + let os_err = std::io::Error::last_os_error(); + return Err(Error::new( + os_err.kind(), + format!("failed to write pid file '{}': {}", path.display(), os_err), + ) + .into()); + } + if rc == 0 { + return Err(Error::new( + ErrorKind::WriteZero, + format!( + "failed to write pid file '{}': write returned 0 bytes", + path.display() + ), + ) + .into()); + } + written += rc as usize; + } + Ok(()) +} + +#[inline] +pub(crate) fn write_pid(postfix: &str) { + let path = std::path::PathBuf::from(get_pid_file(postfix)); + if let Err(err) = write_pid_file(&path) { + log::warn!( + "Failed to write pid file for postfix '{}', path='{}', err={}", + postfix, + path.display(), + err + ); + } +} + +// Purpose: +// - Read pid file safely and avoid trusting symlink/non-regular files. +// +// Approach: +// - Use libc open/fstat/read syscalls (FFI) to control flags and inode checks. +// - Open path with O_NOFOLLOW, validate opened fd via fstat, then read and parse. +// - Keep unsafe scopes minimal and check syscall return values immediately. +// +// Main steps: +// 1) Secure-open pid file read-only. +// 2) Ensure fd points to regular file. +// 3) Read bytes and parse usize pid. +// +// References: +// - open(2): O_NOFOLLOW/O_CLOEXEC/O_NONBLOCK +// https://man7.org/linux/man-pages/man2/open.2.html +// - fstat(2): validate S_IFREG on opened fd +// https://man7.org/linux/man-pages/man2/fstat.2.html +// - read(2): read bytes via fd +// https://man7.org/linux/man-pages/man2/read.2.html +#[inline] +fn read_pid_file_secure(path: &Path) -> Option { + let path_c = CString::new(path.as_os_str().as_bytes().to_vec()).ok()?; + let flags = hbb_common::libc::O_RDONLY + | hbb_common::libc::O_CLOEXEC + | hbb_common::libc::O_NOFOLLOW + | hbb_common::libc::O_NONBLOCK; + let fd = unsafe { hbb_common::libc::open(path_c.as_ptr(), flags) }; + if fd < 0 { + return None; + } + let _fd_guard = FdGuard(fd); + + let mut stat: hbb_common::libc::stat = unsafe { std::mem::zeroed() }; + if unsafe { hbb_common::libc::fstat(fd, &mut stat) } != 0 { + return None; + } + if (stat.st_mode & (hbb_common::libc::S_IFMT as hbb_common::libc::mode_t)) + != (hbb_common::libc::S_IFREG as hbb_common::libc::mode_t) + { + return None; + } + + let mut buffer = [0u8; 64]; + let read_len = unsafe { + hbb_common::libc::read( + fd, + buffer.as_mut_ptr() as *mut hbb_common::libc::c_void, + buffer.len(), + ) + }; + if read_len <= 0 { + return None; + } + let content = String::from_utf8_lossy(&buffer[..read_len as usize]).to_string(); + content.trim().parse::().ok() +} + +#[inline] +async fn probe_existing_listener(postfix: &str) -> bool { + let Ok(mut stream) = connect(1000, postfix).await else { + return false; + }; + if postfix != crate::POSTFIX_SERVICE { + return true; + } + if stream.send(&Data::SyncConfig(None)).await.is_err() { + return false; + } + matches!( + stream.next_timeout(1000).await, + Ok(Some(Data::SyncConfig(Some(_)))) + ) +} + +pub(crate) async fn check_pid(postfix: &str) -> bool { + let pid_file = std::path::PathBuf::from(get_pid_file(postfix)); + if let Some(pid) = read_pid_file_secure(&pid_file) { + if pid > 0 { + let mut sys = hbb_common::sysinfo::System::new(); + sys.refresh_processes(); + if let Some(p) = sys.process(pid.into()) { + if let Some(current) = sys.process((std::process::id() as usize).into()) { + if current.name() == p.name() && probe_existing_listener(postfix).await { + return true; + } + } + } + } + } + if probe_existing_listener(postfix).await { + return true; + } + // if not remove old ipc file, the new ipc creation will fail + // if we remove a ipc file, but the old ipc process is still running, + // new connection to the ipc will connect to new ipc, old connection to old ipc still keep alive + if let Err(err) = remove_ipc_socket_via_secure_parent_fd(postfix) { + log::debug!( + "Failed to remove stale ipc socket via secure parent fd: postfix={}, err={}", + postfix, + err + ); + } + false +} + +#[inline] +pub(crate) fn should_scrub_parent_entries_after_check_pid( + should_scrub_parent_entries: bool, + existing_listener_alive: bool, +) -> bool { + should_scrub_parent_entries && !existing_listener_alive +} + +#[cfg(test)] +mod tests { + #[test] + fn test_write_pid_file_rejects_symlink() { + use std::os::unix::fs::symlink; + + let unique = format!( + "rustdesk-ipc-pid-file-test-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() + ); + let base = std::env::temp_dir().join(unique); + std::fs::create_dir_all(&base).unwrap(); + + let target = base.join("target_pid"); + std::fs::write(&target, b"origin").unwrap(); + let link = base.join("pid_link"); + symlink(&target, &link).unwrap(); + + let res = super::write_pid_file(&link); + assert!(res.is_err()); + assert_eq!(std::fs::read_to_string(&target).unwrap(), "origin"); + + std::fs::remove_file(&link).ok(); + std::fs::remove_file(&target).ok(); + std::fs::remove_dir_all(&base).ok(); + } + + #[test] + fn test_ensure_secure_ipc_parent_dir_rejects_symlink_parent() { + use std::os::unix::fs::symlink; + + let unique = format!( + "rustdesk-ipc-secure-dir-test-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() + ); + let base = std::env::temp_dir().join(unique); + let real_dir = base.join("real"); + let link_dir = base.join("link"); + std::fs::create_dir_all(&real_dir).unwrap(); + symlink(&real_dir, &link_dir).unwrap(); + let ipc_path = link_dir.join("ipc_service"); + let res = + super::ensure_secure_ipc_parent_dir(ipc_path.to_string_lossy().as_ref(), "_service"); + assert!(res.is_err()); + std::fs::remove_file(&link_dir).ok(); + std::fs::remove_dir_all(&real_dir).ok(); + std::fs::remove_dir_all(&base).ok(); + } + + #[test] + fn test_ensure_secure_ipc_parent_dir_creates_parent_with_expected_mode() { + use std::os::unix::fs::PermissionsExt; + + let unique = format!( + "rustdesk-ipc-secure-dir-create-test-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() + ); + let base = std::env::temp_dir().join(unique); + std::fs::create_dir_all(&base).unwrap(); + + // Intentionally choose a parent that does not exist to exercise the ENOENT -> mkdir branch. + let parent_dir = base.join("parent"); + assert!(!parent_dir.exists()); + let ipc_path = parent_dir.join("ipc"); + + let res = super::ensure_secure_ipc_parent_dir(ipc_path.to_string_lossy().as_ref(), ""); + // Restrictive umask can make mkdir create a stricter initial mode. In that case + // ensure_secure_ipc_parent_dir repairs it with fchmod and may request a scrub. + res.unwrap(); + + let md = std::fs::metadata(&parent_dir).unwrap(); + assert!(md.is_dir()); + let mode = md.permissions().mode() & 0o777; + assert_eq!(mode, 0o0700); + + std::fs::remove_dir_all(&base).ok(); + } + + #[test] + fn test_scrub_preexisting_ipc_parent_entries_only_removes_target_postfix_artifacts() { + use std::os::unix::ffi::OsStrExt; + + let unique = format!( + "rustdesk-ipc-scrub-test-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() + ); + let base = std::env::temp_dir().join(unique); + std::fs::create_dir_all(&base).unwrap(); + + let ipc_file = base.join("ipc_service"); + let ipc_pid_file = base.join("ipc_service.pid"); + let ipc_other_postfix_file = base.join("ipc_uinput_1"); + let keep_file = base.join("keep.txt"); + let keep_dir = base.join("keep_dir"); + + std::fs::write(&ipc_file, b"socket-placeholder").unwrap(); + std::fs::write(&ipc_pid_file, b"1234").unwrap(); + std::fs::write(&ipc_other_postfix_file, b"other-postfix").unwrap(); + std::fs::write(&keep_file, b"keep").unwrap(); + std::fs::create_dir_all(&keep_dir).unwrap(); + + let base_c = std::ffi::CString::new(base.as_os_str().as_bytes().to_vec()).unwrap(); + let base_fd = super::open_ipc_parent_dir_fd(&base_c).unwrap(); + let _base_guard = super::FdGuard(base_fd); + super::scrub_preexisting_ipc_parent_entries(base_fd, &base, "_service").unwrap(); + + assert!(!ipc_file.exists()); + assert!(!ipc_pid_file.exists()); + assert!(ipc_other_postfix_file.exists()); + assert!(keep_file.exists()); + assert!(keep_dir.exists()); + + std::fs::remove_file(&ipc_other_postfix_file).ok(); + std::fs::remove_file(&keep_file).ok(); + std::fs::remove_dir_all(&keep_dir).ok(); + std::fs::remove_dir_all(&base).ok(); + } + + #[test] + fn test_scrub_preexisting_ipc_parent_entries_should_bind_to_opened_inode_not_path() { + use std::os::unix::ffi::OsStrExt; + + let unique = format!( + "rustdesk-ipc-scrub-fd-bind-test-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() + ); + let base = std::env::temp_dir().join(unique); + std::fs::create_dir_all(&base).unwrap(); + + let trusted_parent = base.join("trusted_parent"); + let trusted_parent_moved = base.join("trusted_parent_moved"); + let attacker_parent = base.join("attacker_parent"); + std::fs::create_dir_all(&trusted_parent).unwrap(); + std::fs::create_dir_all(&attacker_parent).unwrap(); + + let trusted_ipc_file = trusted_parent.join("ipc_service"); + let attacker_ipc_file = attacker_parent.join("ipc_service"); + std::fs::write(&trusted_ipc_file, b"trusted").unwrap(); + std::fs::write(&attacker_ipc_file, b"attacker").unwrap(); + + let trusted_parent_c = + std::ffi::CString::new(trusted_parent.as_os_str().as_bytes().to_vec()).unwrap(); + let trusted_parent_fd = super::open_ipc_parent_dir_fd(&trusted_parent_c).unwrap(); + let _trusted_parent_guard = super::FdGuard(trusted_parent_fd); + + // Swap the path after the trusted inode has been opened. + std::fs::rename(&trusted_parent, &trusted_parent_moved).unwrap(); + std::fs::rename(&attacker_parent, &trusted_parent).unwrap(); + + super::scrub_preexisting_ipc_parent_entries(trusted_parent_fd, &trusted_parent, "_service") + .unwrap(); + + // Expected secure behavior: scrub should target the inode that was opened before path swap. + assert!( + !trusted_parent_moved.join("ipc_service").exists(), + "trusted inode artifact should be removed even after path swap" + ); + assert!( + trusted_parent.join("ipc_service").exists(), + "path-swapped attacker directory should not be scrubbed" + ); + + std::fs::remove_dir_all(&base).ok(); + } + + #[test] + fn test_ensure_secure_ipc_parent_dir_keeps_service_artifacts_before_liveness_probe() { + use std::os::unix::fs::PermissionsExt; + + let unique = format!( + "rustdesk-ipc-secure-dir-order-test-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() + ); + let base = std::env::temp_dir().join(unique); + std::fs::create_dir_all(&base).unwrap(); + + let parent_dir = base.join("service_parent"); + std::fs::create_dir_all(&parent_dir).unwrap(); + // Trigger "had_untrusted_service_parent_mode". + std::fs::set_permissions(&parent_dir, std::fs::Permissions::from_mode(0o777)).unwrap(); + + let ipc_file = parent_dir.join("ipc_service"); + let ipc_pid_file = parent_dir.join("ipc_service.pid"); + std::fs::write(&ipc_file, b"socket-placeholder").unwrap(); + std::fs::write(&ipc_pid_file, b"1234").unwrap(); + + let res = + super::ensure_secure_ipc_parent_dir(ipc_file.to_string_lossy().as_ref(), "_service"); + assert_eq!(res.unwrap(), true); + + // Parent hardening should run first; artifacts should stay until liveness probe completes. + assert!(ipc_file.exists(), "ipc socket marker should be preserved"); + assert!(ipc_pid_file.exists(), "pid marker should be preserved"); + + std::fs::remove_dir_all(&base).ok(); + } + + #[test] + fn test_ensure_secure_ipc_parent_dir_marks_non_service_mode_repair_for_scrub() { + use std::os::unix::fs::PermissionsExt; + + let unique = format!( + "rustdesk-ipc-nonservice-mode-repair-test-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() + ); + let base = std::env::temp_dir().join(unique); + std::fs::create_dir_all(&base).unwrap(); + + let parent_dir = base.join("non_service_parent"); + std::fs::create_dir_all(&parent_dir).unwrap(); + std::fs::set_permissions(&parent_dir, std::fs::Permissions::from_mode(0o755)).unwrap(); + + let ipc_file = parent_dir.join("ipc"); + std::fs::write(&ipc_file, b"socket-placeholder").unwrap(); + + let res = super::ensure_secure_ipc_parent_dir(ipc_file.to_string_lossy().as_ref(), ""); + assert_eq!(res.unwrap(), true); + + std::fs::remove_dir_all(&base).ok(); + } + + #[test] + fn test_should_scrub_parent_entries_after_check_pid_only_when_requested_and_not_alive() { + assert!(!super::should_scrub_parent_entries_after_check_pid( + false, false + )); + assert!(!super::should_scrub_parent_entries_after_check_pid( + false, true + )); + assert!(super::should_scrub_parent_entries_after_check_pid( + true, false + )); + assert!(!super::should_scrub_parent_entries_after_check_pid( + true, true + )); + } +} diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 7157da760..9a4bb37ec 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -29,6 +29,12 @@ use wallpaper; pub const PA_SAMPLE_RATE: u32 = 48000; static mut UNMODIFIED: bool = true; +#[derive(Clone, Debug)] +struct ActiveUserLookupCache { + uid: String, + username: String, +} + const INVALID_TERM_VALUES: [&str; 3] = ["", "unknown", "dumb"]; const SHELL_PROCESSES: [&str; 4] = ["bash", "zsh", "fish", "sh"]; @@ -50,6 +56,8 @@ lazy_static::lazy_static! { } } }; + static ref ACTIVE_USER_LOOKUP_CACHE: std::sync::Mutex> = + std::sync::Mutex::new(None); // https://github.com/rustdesk/rustdesk/issues/13705 // Check if `sudo -E` actually preserves environment. // @@ -82,6 +90,27 @@ lazy_static::lazy_static! { }; } +#[inline] +fn update_active_user_lookup_cache(desktop: &Desktop) { + if let Ok(mut cache) = ACTIVE_USER_LOOKUP_CACHE.lock() { + if desktop.uid.is_empty() || desktop.username.is_empty() { + *cache = None; + } else { + *cache = Some(ActiveUserLookupCache { + uid: desktop.uid.clone(), + username: desktop.username.clone(), + }); + } + } +} + +#[inline] +fn get_active_user_id_name_from_cache() -> Option<(String, String)> { + let cache = ACTIVE_USER_LOOKUP_CACHE.lock().ok()?; + let entry = cache.as_ref()?; + Some((entry.uid.clone(), entry.username.clone())) +} + thread_local! { // XDO context - created via libxdo-sys (which uses dynamic loading stub). // If libxdo is not available, xdo will be null and xdo-based functions become no-ops. @@ -789,6 +818,7 @@ pub fn start_os_service() { let mut last_restart = Instant::now(); while running.load(Ordering::SeqCst) { desktop.refresh(); + update_active_user_lookup_cache(&desktop); // Duplicate logic here with should_start_server // Login wayland will try to start a headless --server. @@ -861,13 +891,29 @@ pub fn start_os_service() { } #[inline] +/// Returns the cached active `(uid, username)` snapshot when available. +/// Callers that require a fresh seat0 lookup should call `get_values_of_seat0` directly. pub fn get_active_user_id_name() -> (String, String) { + if let Some(id_name) = get_active_user_id_name_from_cache() { + return id_name; + } let vec_id_name = get_values_of_seat0(&[1, 2]); (vec_id_name[0].clone(), vec_id_name[1].clone()) } #[inline] +/// Returns the cached active uid when available. +/// Callers that require a fresh seat0 lookup should call `get_values_of_seat0` directly. pub fn get_active_userid() -> String { + if let Some((uid, _)) = get_active_user_id_name_from_cache() { + return uid; + } + get_values_of_seat0(&[1])[0].clone() +} + +#[inline] +/// Returns the active uid from a fresh seat0 lookup, bypassing the service-loop cache. +pub fn get_active_userid_fresh() -> String { get_values_of_seat0(&[1])[0].clone() } @@ -922,7 +968,12 @@ fn _get_display_manager() -> String { } #[inline] +/// Returns the cached active username when available. +/// Callers that require a fresh seat0 lookup should call `get_values_of_seat0` directly. pub fn get_active_username() -> String { + if let Some((_, username)) = get_active_user_id_name_from_cache() { + return username; + } get_values_of_seat0(&[2])[0].clone() } diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 4c09bbe9f..a755714f9 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -73,10 +73,19 @@ use winapi::{ }; use windows::Win32::{ Foundation::{CloseHandle as WinCloseHandle, HANDLE as WinHANDLE}, + Security::{ + GetTokenInformation as WinGetTokenInformation, IsWellKnownSid, TokenUser, + WinLocalSystemSid, TOKEN_QUERY as WIN_TOKEN_QUERY, TOKEN_USER, + }, System::Diagnostics::ToolHelp::{ CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W, TH32CS_SNAPPROCESS, }, + System::Threading::{ + OpenProcess as WinOpenProcess, OpenProcessToken as WinOpenProcessToken, + QueryFullProcessImageNameW as WinQueryFullProcessImageNameW, + PROCESS_QUERY_LIMITED_INFORMATION as WIN_PROCESS_QUERY_LIMITED_INFORMATION, + }, }; use windows_service::{ define_windows_service, @@ -88,6 +97,14 @@ use windows_service::{ }; use winreg::{enums::*, RegKey}; +mod acl; +pub(crate) use acl::current_process_user_sid_string; +pub use acl::{ + set_path_permission, set_path_permission_for_portable_service_shmem_dir, + set_path_permission_for_portable_service_shmem_file, + validate_path_for_portable_service_shmem_dir, +}; + pub const FLUTTER_RUNNER_WIN32_WINDOW_CLASS: &'static str = "FLUTTER_RUNNER_WIN32_WINDOW"; // main window, install window pub const EXPLORER_EXE: &'static str = "explorer.exe"; pub const SET_FOREGROUND_WINDOW: &'static str = "SET_FOREGROUND_WINDOW"; @@ -565,6 +582,55 @@ pub fn get_current_session_id(share_rdp: bool) -> DWORD { unsafe { get_current_session(if share_rdp { TRUE } else { FALSE }) } } +#[inline] +fn resolve_expected_active_session_id_for_service(session_id: u32) -> Option { + let share_rdp_enabled = is_share_rdp(); + if get_available_sessions(false) + .iter() + .any(|e| e.sid == session_id) + { + return Some(session_id); + } + let current_active_session = + unsafe { get_current_session(if share_rdp_enabled { TRUE } else { FALSE }) }; + if current_active_session == u32::MAX { + None + } else { + Some(current_active_session) + } +} + +#[inline] +fn authorize_service_scoped_ipc_connection( + stream: &ipc::Connection, + expected_active_session_id: Option, +) -> bool { + let (authorized, peer_pid, peer_session_id, peer_is_system) = + stream.service_authorization_status_for_session(expected_active_session_id); + if !authorized { + ipc::log_rejected_windows_ipc_connection( + crate::POSTFIX_SERVICE, + peer_pid, + peer_session_id, + expected_active_session_id, + peer_is_system, + ); + return false; + } + if let Err(err) = + ipc::ensure_peer_executable_matches_current_by_pid_opt(peer_pid, crate::POSTFIX_SERVICE) + { + log::warn!( + "Rejected unauthorized connection on protected service-scoped IPC channel due to executable mismatch: postfix={}, peer_pid={:?}, err={}", + crate::POSTFIX_SERVICE, + peer_pid, + err + ); + return false; + } + true +} + extern "system" { fn BlockInput(v: BOOL) -> BOOL; } @@ -631,6 +697,15 @@ async fn run_service(_arguments: Vec) -> ResultType<()> { Ok(res) => match res { Some(Ok(stream)) => { let mut stream = ipc::Connection::new(stream); + // Keep IPC authorization consistent with the session we are currently serving. + // Recompute expected session right before authorization to avoid using a stale + // session_id after awaiting incoming.next(). + let expected_active_session_id = + resolve_expected_active_session_id_for_service(session_id); + if !authorize_service_scoped_ipc_connection(&stream, expected_active_session_id) + { + continue; + } if let Ok(Some(data)) = stream.next_timeout(1000).await { match data { ipc::Data::Close => { @@ -1141,6 +1216,22 @@ pub fn get_active_user_home() -> Option { None } +#[cfg(not(feature = "flutter"))] +#[inline] +pub fn portable_service_logon_helper_paths() -> Option<(PathBuf, PathBuf)> { + // Keep parity with history for now: derive LocalAppData from user profile path. + // If users report redirected/non-standard LocalAppData issues, switch to: + // `BaseDirs::new()?.data_local_dir()` for Known Folder-based resolution. + let user_dir = hbb_common::directories_next::UserDirs::new()?; + let dir = user_dir + .home_dir() + .join("AppData") + .join("Local") + .join("rustdesk-sciter"); + let dst = dir.join("rustdesk.exe"); + Some((dir, dst)) +} + pub fn is_prelogin() -> bool { let Some(username) = get_current_session_username() else { return false; @@ -2327,16 +2418,33 @@ pub fn elevate_or_run_as_system(is_setup: bool, is_elevate: bool, is_run_as_syst is_run_as_system, crate::username(), ); - let arg_elevate = if is_setup { + let mut arg_elevate = if is_setup { "--noinstall --elevate" } else { "--elevate" - }; - let arg_run_as_system = if is_setup { + } + .to_owned(); + let mut arg_run_as_system = if is_setup { "--noinstall --run-as-system" } else { "--run-as-system" - }; + } + .to_owned(); + let shmem_name_from_args = crate::portable_service::portable_service_shmem_name_from_args(); + if shmem_name_from_args.is_none() && crate::portable_service::has_portable_service_shmem_arg() { + log::error!("Invalid portable service shared memory argument, aborting elevation flow"); + // This is a malformed bootstrap argument in a privilege-sensitive path. + // Keep fail-closed process termination here to avoid continuing elevation + // with inconsistent shared-memory contract. + std::process::exit(1); + } + if let Some(shmem_name) = shmem_name_from_args { + let shmem_arg = crate::portable_service::portable_service_shmem_arg(&shmem_name); + arg_elevate.push(' '); + arg_elevate.push_str(&shmem_arg); + arg_run_as_system.push(' '); + arg_run_as_system.push_str(&shmem_arg); + } if is_root() { if is_run_as_system { log::info!("run portable service"); @@ -2347,7 +2455,7 @@ pub fn elevate_or_run_as_system(is_setup: bool, is_elevate: bool, is_run_as_syst Ok(elevated) => { if elevated { if !is_run_as_system { - if run_as_system(arg_run_as_system).is_ok() { + if run_as_system(arg_run_as_system.as_str()).is_ok() { std::process::exit(0); } else { log::error!( @@ -2358,7 +2466,7 @@ pub fn elevate_or_run_as_system(is_setup: bool, is_elevate: bool, is_run_as_syst } } else { if !is_elevate { - if let Ok(true) = elevate(arg_elevate) { + if let Ok(true) = elevate(arg_elevate.as_str()) { std::process::exit(0); } else { log::error!("Failed to elevate, error {}", io::Error::last_os_error()); @@ -2416,6 +2524,115 @@ pub fn is_elevated(process_id: Option) -> ResultType { } } +#[inline] +unsafe fn read_token_user_buffer(token: WinHANDLE, subject: &str) -> ResultType> { + let mut token_user_size = 0u32; + let get_info_result = WinGetTokenInformation(token, TokenUser, None, 0, &mut token_user_size); + match get_info_result { + Ok(()) => { + if token_user_size == 0 { + bail!( + "Failed to get {} token user size: unexpected zero buffer size", + subject + ); + } + } + Err(e) => { + // Allow expected size-probe failures if Windows still returns required size. + let is_insufficient_buffer = + e.code() == windows::core::HRESULT::from_win32(ERROR_INSUFFICIENT_BUFFER as u32); + let is_bad_length = + e.code() == windows::core::HRESULT::from_win32(ERROR_BAD_LENGTH as u32); + if (!is_insufficient_buffer && !is_bad_length) || token_user_size == 0 { + bail!("Failed to get {} token user size: {}", subject, e); + } + } + } + + let mut buffer = vec![0u8; token_user_size as usize]; + WinGetTokenInformation( + token, + TokenUser, + Some(buffer.as_mut_ptr() as *mut core::ffi::c_void), + token_user_size, + &mut token_user_size, + ) + .map_err(|e| anyhow!("Failed to get {} token user: {}", subject, e))?; + + let min_size = std::mem::size_of::(); + if buffer.len() < min_size { + bail!( + "Failed to parse {} token user: buffer too small (got {}, need >= {})", + subject, + buffer.len(), + min_size + ); + } + Ok(buffer) +} + +/// Similar to `is_root()` / `is_local_system()` but for an arbitrary process. +/// +/// Returns `true` if the target process is running as LocalSystem (SID: S-1-5-18). +/// +/// TODO: After a few releases of real-world validation, consider replacing +/// the legacy `is_local_system()` with this implementation. +pub fn is_process_running_as_system(process_id: DWORD) -> ResultType { + unsafe { + let process = WinOpenProcess(WIN_PROCESS_QUERY_LIMITED_INFORMATION, false, process_id) + .map_err(|e| anyhow!("Failed to open process {}: {}", process_id, e))?; + + let mut token = WinHANDLE::default(); + let result = (|| -> ResultType { + WinOpenProcessToken(process, WIN_TOKEN_QUERY, &mut token) + .map_err(|e| anyhow!("Failed to open process {} token: {}", process_id, e))?; + + let token_subject = format!("process {}", process_id); + let buffer = read_token_user_buffer(token, token_subject.as_str())?; + let token_user: TOKEN_USER = + std::ptr::read_unaligned(buffer.as_ptr() as *const TOKEN_USER); + Ok(IsWellKnownSid(token_user.User.Sid, WinLocalSystemSid).as_bool()) + })(); + + if !token.is_invalid() { + let _ = WinCloseHandle(token); + } + let _ = WinCloseHandle(process); + result + } +} + +pub fn get_process_executable_path(process_id: DWORD) -> ResultType { + const PROCESS_IMAGE_PATH_BUFFER_LEN: usize = 32 * 1024; + unsafe { + let process = WinOpenProcess(WIN_PROCESS_QUERY_LIMITED_INFORMATION, false, process_id) + .map_err(|e| anyhow!("Failed to open process {}: {}", process_id, e))?; + + let result = (|| -> ResultType { + let mut buffer = vec![0u16; PROCESS_IMAGE_PATH_BUFFER_LEN]; + let mut length = PROCESS_IMAGE_PATH_BUFFER_LEN as u32; + WinQueryFullProcessImageNameW( + process, + windows::Win32::System::Threading::PROCESS_NAME_FORMAT(0), + windows::core::PWSTR(buffer.as_mut_ptr()), + &mut length, + ) + .map_err(|e| anyhow!("Failed to query process {} image path: {}", process_id, e))?; + if length == 0 { + bail!( + "Failed to query process {} image path: empty result", + process_id + ); + } + buffer.truncate(length as usize); + Ok(PathBuf::from(OsString::from_wide(&buffer))) + })(); + + let _ = WinCloseHandle(process); + result + } +} + pub fn is_foreground_window_elevated() -> ResultType { unsafe { let mut process_id: DWORD = 0; @@ -2708,16 +2925,6 @@ pub fn create_process_with_logon(user: &str, pwd: &str, exe: &str, arg: &str) -> return Ok(()); } -pub fn set_path_permission(dir: &Path, permission: &str) -> ResultType<()> { - std::process::Command::new("icacls") - .arg(dir.as_os_str()) - .arg("/grant") - .arg(format!("*S-1-1-0:(OI)(CI){}", permission)) - .arg("/T") - .spawn()?; - Ok(()) -} - #[inline] fn str_to_device_name(name: &str) -> [u16; 32] { let mut device_name: Vec = wide_string(name); @@ -4281,6 +4488,87 @@ pub(super) fn get_pids_with_first_arg_by_wmic, S2: AsRef>( #[cfg(test)] mod tests { use super::*; + + // Test-only reusable Win32 HANDLE RAII helper. + // If a future non-test path needs the same pattern, move it out of this test module. + // + // This struct is similar to `hbb_common::platform::windows::RAIIHandle`, + // but `RAIIHandle` depends on `WinApi` crate, while this `HandleGuard` only depends on `windows` crate. + struct HandleGuard(WinHANDLE); + + impl HandleGuard { + #[inline] + fn new(handle: WinHANDLE) -> Self { + Self(handle) + } + + #[inline] + fn get(&self) -> WinHANDLE { + self.0 + } + } + + impl Drop for HandleGuard { + fn drop(&mut self) { + unsafe { + if !self.0.is_invalid() { + let _ = WinCloseHandle(self.0); + } + } + } + } + + #[test] + fn test_is_process_running_as_system_invalid_pid_errors() { + assert!(is_process_running_as_system(u32::MAX).is_err()); + } + + #[test] + fn test_is_process_running_as_system_matches_current_process_token_user() { + let pid = unsafe { windows::Win32::System::Threading::GetCurrentProcessId() }; + let actual = is_process_running_as_system(pid).unwrap(); + + let expected = unsafe { + // Keep this test consistent: use only the `windows` crate APIs/types. + let process = HandleGuard::new( + WinOpenProcess(WIN_PROCESS_QUERY_LIMITED_INFORMATION, false, pid) + .expect("WinOpenProcess should succeed for current process"), + ); + let mut token = WinHANDLE::default(); + WinOpenProcessToken(process.get(), WIN_TOKEN_QUERY, &mut token) + .expect("WinOpenProcessToken should succeed for current process"); + let token = HandleGuard::new(token); + + let mut token_user_size = 0u32; + let _ = WinGetTokenInformation(token.get(), TokenUser, None, 0, &mut token_user_size); + assert_ne!(token_user_size, 0, "TokenUser size should be non-zero"); + + let mut buffer = vec![0u8; token_user_size as usize]; + WinGetTokenInformation( + token.get(), + TokenUser, + Some(buffer.as_mut_ptr() as *mut core::ffi::c_void), + token_user_size, + &mut token_user_size, + ) + .expect("WinGetTokenInformation(TokenUser) should succeed for current process"); + + let min_size = std::mem::size_of::(); + assert!( + buffer.len() >= min_size, + "TokenUser buffer too small (got {}, need >= {})", + buffer.len(), + min_size + ); + let token_user: TOKEN_USER = + std::ptr::read_unaligned(buffer.as_ptr() as *const TOKEN_USER); + let expected = IsWellKnownSid(token_user.User.Sid, WinLocalSystemSid).as_bool(); + expected + }; + + assert_eq!(actual, expected); + } + #[test] fn test_uninstall_cert() { println!("uninstall driver certs: {:?}", cert::uninstall_cert()); diff --git a/src/platform/windows/acl.rs b/src/platform/windows/acl.rs new file mode 100644 index 000000000..682e66fed --- /dev/null +++ b/src/platform/windows/acl.rs @@ -0,0 +1,903 @@ +// https://learn.microsoft.com/en-us/windows/win32/secgloss/security-glossary + +use super::{read_token_user_buffer, wide_string, ResultType}; +use hbb_common::{anyhow::anyhow, bail}; +use std::{ + fs, io, + os::windows::{ffi::OsStrExt, fs::MetadataExt}, + path::Path, +}; +use windows::{ + core::{PCWSTR, PWSTR}, + Win32::{ + Foundation::{CloseHandle, LocalFree, HANDLE, HLOCAL}, + Security::{ + Authorization::{ + ConvertSidToStringSidW, ConvertStringSidToSidW, GetNamedSecurityInfoW, + SetEntriesInAclW, SetNamedSecurityInfoW, EXPLICIT_ACCESS_W, SET_ACCESS, + SE_FILE_OBJECT, TRUSTEE_IS_GROUP, TRUSTEE_IS_SID, TRUSTEE_IS_USER, TRUSTEE_W, + }, + ACE_FLAGS, ACL, CONTAINER_INHERIT_ACE, DACL_SECURITY_INFORMATION, NO_INHERITANCE, + OBJECT_INHERIT_ACE, PROTECTED_DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, PSID, + TOKEN_QUERY, TOKEN_USER, + }, + Storage::FileSystem::{FILE_ALL_ACCESS, FILE_GENERIC_WRITE}, + System::Threading::{GetCurrentProcess, OpenProcessToken}, + }, +}; + +const FILE_ATTRIBUTE_REPARSE_POINT_U32: u32 = 0x400; + +#[inline] +fn is_reparse_point(metadata: &fs::Metadata) -> bool { + (metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT_U32) != 0 +} + +fn apply_grant_sid_allow_ace_to_path( + path: &Path, + sid_ptr: *mut std::ffi::c_void, + access_mask: u32, + is_group: bool, + is_dir: bool, +) -> ResultType<()> { + // Merge mode: read existing DACL and append/replace ACE via SetEntriesInAclW. + // https://learn.microsoft.com/en-us/windows/win32/secauthz/modifying-the-acls-of-an-object-in-c-- + let mut old_dacl: *mut ACL = std::ptr::null_mut(); + let mut security_descriptor = PSECURITY_DESCRIPTOR::default(); + let path_utf16: Vec = path + .as_os_str() + .encode_wide() + .chain(std::iter::once(0)) + .collect(); + let get_named_result = unsafe { + GetNamedSecurityInfoW( + PCWSTR::from_raw(path_utf16.as_ptr()), + SE_FILE_OBJECT, + DACL_SECURITY_INFORMATION, + None, + None, + Some(&mut old_dacl), + None, + &mut security_descriptor, + ) + }; + if get_named_result.0 != 0 { + bail!( + "GetNamedSecurityInfoW failed for '{}': win32_error={}", + path.display(), + get_named_result.0 + ); + } + let _sd_guard = LocalAllocGuard(security_descriptor.0); + + let inherit_flags = if is_dir { + ACE_FLAGS(OBJECT_INHERIT_ACE.0 | CONTAINER_INHERIT_ACE.0) + } else { + NO_INHERITANCE + }; + let explicit_access = [make_sid_trustee_entry( + sid_ptr, + access_mask, + inherit_flags, + is_group, + )]; + let old_acl_option = if old_dacl.is_null() { + None + } else { + Some(old_dacl as *const ACL) + }; + let mut new_acl: *mut ACL = std::ptr::null_mut(); + let set_entries_result = unsafe { + SetEntriesInAclW( + Some(explicit_access.as_slice()), + old_acl_option, + &mut new_acl, + ) + }; + if set_entries_result.0 != 0 { + bail!( + "SetEntriesInAclW failed for '{}': win32_error={}", + path.display(), + set_entries_result.0 + ); + } + if new_acl.is_null() { + bail!( + "SetEntriesInAclW returned null ACL for '{}'", + path.display() + ); + } + let _acl_guard = LocalAllocGuard(new_acl as *mut std::ffi::c_void); + + let set_named_result = unsafe { + SetNamedSecurityInfoW( + PCWSTR::from_raw(path_utf16.as_ptr()), + SE_FILE_OBJECT, + DACL_SECURITY_INFORMATION, + None, + None, + Some(new_acl), + None, + ) + }; + if set_named_result.0 != 0 { + bail!( + "SetNamedSecurityInfoW failed for '{}': win32_error={}", + path.display(), + set_named_result.0 + ); + } + Ok(()) +} + +/// Grants `Everyone` on `dir` recursively for helper/runtime files that must be +/// readable/executable across user contexts. +/// +/// `access_mask` is the Win32 file access mask to grant recursively. +pub fn set_path_permission(dir: &Path, access_mask: u32) -> ResultType<()> { + let metadata = fs::symlink_metadata(dir).map_err(|e| { + anyhow!( + "Failed to inspect ACL target directory '{}': {}", + dir.display(), + e + ) + })?; + if is_reparse_point(&metadata) { + bail!( + "ACL target directory is a reparse point and is rejected: '{}'", + dir.display() + ); + } + if !metadata.file_type().is_dir() { + bail!("ACL target is not a directory: '{}'", dir.display()); + } + + let everyone_sid = sid_string_to_local_alloc_guard("S-1-1-0")?; + let mut stack = vec![dir.to_path_buf()]; + while let Some(path) = stack.pop() { + let metadata = fs::symlink_metadata(&path) + .map_err(|e| anyhow!("Failed to inspect ACL target '{}': {}", path.display(), e))?; + if is_reparse_point(&metadata) { + continue; + } + let is_dir = metadata.file_type().is_dir(); + apply_grant_sid_allow_ace_to_path( + &path, + everyone_sid.as_sid_ptr(), + access_mask, + true, + is_dir, + )?; + if !is_dir { + continue; + } + for entry in fs::read_dir(&path) + .map_err(|e| anyhow!("Failed to list ACL target dir '{}': {}", path.display(), e))? + { + let entry = entry.map_err(|e| { + anyhow!( + "Failed to read ACL target dir entry under '{}': {}", + path.display(), + e + ) + })?; + stack.push(entry.path()); + } + } + Ok(()) +} + +/// Returns the current process user SID as a standard SID string +/// (for example: `S-1-5-18`). +/// +/// Source: +/// - Official SID-to-string API (`ConvertSidToStringSidW`): +/// https://learn.microsoft.com/en-us/windows/win32/api/sddl/nf-sddl-convertsidtostringsidw +pub(crate) fn current_process_user_sid_string() -> ResultType { + let mut token = HANDLE::default(); + let result = (|| -> ResultType { + unsafe { + OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) + .map_err(|e| anyhow!("Failed to open current process token: {}", e))?; + } + + let buffer = unsafe { read_token_user_buffer(token, "current process")? }; + let token_user: TOKEN_USER = + unsafe { std::ptr::read_unaligned(buffer.as_ptr() as *const TOKEN_USER) }; + if token_user.User.Sid.0.is_null() { + bail!("Token SID is null"); + } + + let mut sid_string_ptr = PWSTR::null(); + unsafe { + ConvertSidToStringSidW(token_user.User.Sid, &mut sid_string_ptr).map_err(|e| { + anyhow!( + "ConvertSidToStringSidW failed for current process token SID: {}", + e + ) + })?; + } + if sid_string_ptr.is_null() { + bail!("ConvertSidToStringSidW returned null SID string pointer"); + } + let _sid_string_guard = LocalAllocGuard(sid_string_ptr.0 as *mut std::ffi::c_void); + unsafe { + sid_string_ptr + .to_string() + .map_err(|e| anyhow!("Failed to decode SID string as UTF-16: {}", e)) + } + })(); + + if !token.is_invalid() { + unsafe { + let _ = CloseHandle(token); + } + } + result +} + +/// Hardens ACLs for portable-service shared-memory path (directory or file). +/// +/// Why: +/// - Shared memory used by portable service carries runtime control/data and must not inherit +/// broad/default ACLs. +/// - We explicitly grant only trusted principals and remove broad groups to reduce local +/// privilege-boundary bypass risk. +/// +/// ACL policy applied via Win32 ACL APIs (`SetEntriesInAclW` + `SetNamedSecurityInfoW`): +/// - common (directory + file): +/// - `S-1-5-18` (LocalSystem): full control +/// - `S-1-5-32-544` (Built-in Administrators): full control +/// - `current_process_user_sid_string()` result: full control +/// - directory (`portable_service_shmem` parent): +/// - keep `Authenticated Users` directory-level write so other local accounts can +/// create their own runtime shmem files after account switching +/// - `FILE_GENERIC_WRITE + NO_INHERITANCE` means write/create on this directory itself; +/// it is intentionally not inherited by children. +/// Reference: +/// - File access rights: +/// https://learn.microsoft.com/en-us/windows/win32/fileio/file-access-rights-constants +/// - ACE inheritance rules: +/// https://learn.microsoft.com/en-us/windows/win32/secauthz/ace-inheritance-rules +/// - remove `Everyone` and `Users` grants +/// - file (`shared_memory*` flink): +/// - remove broad grants: +/// - `S-1-1-0` (Everyone) +/// - `S-1-5-11` (Authenticated Users) +/// - `S-1-5-32-545` (Users) +/// +/// https://learn.microsoft.com/en-us/windows/win32/secauthz/well-known-sids +pub fn set_path_permission_for_portable_service_shmem_dir(path: &Path) -> ResultType<()> { + set_path_permission_for_portable_service_shmem_impl(path, true) +} + +#[inline] +pub fn validate_path_for_portable_service_shmem_dir(path: &Path) -> ResultType<()> { + validate_portable_service_shmem_dir_target(path) +} + +#[inline] +pub fn set_path_permission_for_portable_service_shmem_file(path: &Path) -> ResultType<()> { + set_path_permission_for_portable_service_shmem_impl(path, false) +} + +#[derive(Debug)] +pub(super) struct LocalAllocGuard(*mut std::ffi::c_void); + +impl LocalAllocGuard { + #[inline] + pub(super) fn as_sid_ptr(&self) -> *mut std::ffi::c_void { + self.0 + } +} + +impl Drop for LocalAllocGuard { + fn drop(&mut self) { + if self.0.is_null() { + return; + } + // Buffers returned by ConvertStringSidToSidW / SetEntriesInAclW / + // ConvertSidToStringSidW are LocalAlloc-owned and must be LocalFree'ed. + unsafe { + let _ = LocalFree(Some(HLOCAL(self.0))); + } + } +} + +#[inline] +pub(super) fn sid_string_to_local_alloc_guard(sid: &str) -> ResultType { + let sid_utf16 = wide_string(sid); + let mut sid_ptr = PSID::default(); + unsafe { + ConvertStringSidToSidW(PCWSTR::from_raw(sid_utf16.as_ptr()), &mut sid_ptr) + .map_err(|e| anyhow!("ConvertStringSidToSidW failed for '{}': {}", sid, e))?; + } + if sid_ptr.0.is_null() { + bail!("ConvertStringSidToSidW returned null SID for '{}'", sid); + } + Ok(LocalAllocGuard(sid_ptr.0)) +} + +#[inline] +fn make_sid_trustee_entry( + sid_ptr: *mut std::ffi::c_void, + access_permissions: u32, + inheritance: ACE_FLAGS, + is_group: bool, +) -> EXPLICIT_ACCESS_W { + // `is_group` is explicitly provided by the caller from the concrete SID semantic + // (e.g. Administrators/Authenticated Users => group, LocalSystem/current user => user). + EXPLICIT_ACCESS_W { + grfAccessPermissions: access_permissions, + grfAccessMode: SET_ACCESS, + grfInheritance: inheritance, + Trustee: TRUSTEE_W { + pMultipleTrustee: std::ptr::null_mut(), + MultipleTrusteeOperation: Default::default(), + TrusteeForm: TRUSTEE_IS_SID, + TrusteeType: if is_group { + TRUSTEE_IS_GROUP + } else { + TRUSTEE_IS_USER + }, + // SAFETY: With TrusteeForm=TRUSTEE_IS_SID, ptstrName is interpreted as PSID. + ptstrName: PWSTR::from_raw(sid_ptr as *mut u16), + }, + } +} + +fn validate_portable_service_shmem_dir_target(path: &Path) -> ResultType<()> { + let metadata = fs::symlink_metadata(path).map_err(|e| { + anyhow!( + "Failed to inspect portable service shared-memory ACL directory '{}': {}", + path.display(), + e + ) + })?; + if is_reparse_point(&metadata) { + bail!( + "Portable service shared-memory ACL directory target is a reparse point and is rejected: '{}'", + path.display() + ); + } + if !metadata.file_type().is_dir() { + bail!( + "Portable service shared-memory ACL target is not a directory: '{}'", + path.display() + ); + } + Ok(()) +} + +fn set_path_permission_for_portable_service_shmem_impl( + path: &Path, + expect_dir: bool, +) -> ResultType<()> { + if expect_dir { + validate_portable_service_shmem_dir_target(path)?; + } else { + let metadata_result = fs::symlink_metadata(path); + match metadata_result { + Ok(metadata) => { + if metadata.file_type().is_dir() { + bail!( + "Portable service shared-memory ACL target is a directory, expected file-like path: '{}'", + path.display() + ); + } + if is_reparse_point(&metadata) { + bail!( + "Portable service shared-memory ACL file target is a reparse point and is rejected: '{}'", + path.display() + ); + } + } + Err(e) + if e.kind() == io::ErrorKind::NotFound + || e.kind() == io::ErrorKind::PermissionDenied => + { + // Keep going and let Win32 ACL APIs return the final OS error. + // `Path::exists()/is_file()` and metadata can collapse ACL-denied paths into + // a false "not found" signal under restricted directory ACLs. + } + Err(e) => { + bail!( + "Failed to inspect portable service shared-memory ACL target '{}': {}", + path.display(), + e + ); + } + } + } + + let user_sid = current_process_user_sid_string()?; + let local_system_sid = sid_string_to_local_alloc_guard("S-1-5-18")?; + let administrators_sid = sid_string_to_local_alloc_guard("S-1-5-32-544")?; + let current_user_sid = sid_string_to_local_alloc_guard(&user_sid)?; + let authenticated_users_sid = if expect_dir { + Some(sid_string_to_local_alloc_guard("S-1-5-11")?) + } else { + None + }; + + let inherit_flags = if expect_dir { + ACE_FLAGS(OBJECT_INHERIT_ACE.0 | CONTAINER_INHERIT_ACE.0) + } else { + NO_INHERITANCE + }; + let mut entries = vec![ + make_sid_trustee_entry( + local_system_sid.as_sid_ptr(), + FILE_ALL_ACCESS.0, + inherit_flags, + false, + ), + make_sid_trustee_entry( + administrators_sid.as_sid_ptr(), + FILE_ALL_ACCESS.0, + inherit_flags, + true, + ), + make_sid_trustee_entry( + current_user_sid.as_sid_ptr(), + FILE_ALL_ACCESS.0, + inherit_flags, + false, + ), + ]; + if let Some(auth_sid) = authenticated_users_sid.as_ref() { + // Keep the shared parent directory multi-user writable at directory level. + entries.push(make_sid_trustee_entry( + auth_sid.as_sid_ptr(), + FILE_GENERIC_WRITE.0, + NO_INHERITANCE, + true, + )); + } + + // Rebuild mode: build a fresh DACL (old ACL not merged) and apply as protected. + // This avoids carrying over broad legacy ACEs from inherited/default ACLs. + // Reference: + // - SetEntriesInAclW: + // https://learn.microsoft.com/en-us/windows/win32/api/aclapi/nf-aclapi-setentriesinaclw + // - SetNamedSecurityInfoW (PROTECTED_DACL_SECURITY_INFORMATION): + // https://learn.microsoft.com/en-us/windows/win32/api/aclapi/nf-aclapi-setnamedsecurityinfow + let mut new_acl: *mut ACL = std::ptr::null_mut(); + let set_entries_result = + unsafe { SetEntriesInAclW(Some(entries.as_slice()), None, &mut new_acl) }; + if set_entries_result.0 != 0 { + bail!( + "SetEntriesInAclW failed for '{}': win32_error={}", + path.display(), + set_entries_result.0 + ); + } + if new_acl.is_null() { + bail!( + "SetEntriesInAclW returned null ACL for '{}'", + path.display() + ); + } + let _acl_guard = LocalAllocGuard(new_acl as *mut std::ffi::c_void); + + let path_utf16: Vec = path + .as_os_str() + .encode_wide() + .chain(std::iter::once(0)) + .collect(); + let security_info = DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION; + let set_named_result = unsafe { + SetNamedSecurityInfoW( + PCWSTR::from_raw(path_utf16.as_ptr()), + SE_FILE_OBJECT, + security_info, + None, + None, + Some(new_acl), + None, + ) + }; + if set_named_result.0 != 0 { + bail!( + "SetNamedSecurityInfoW failed for '{}': win32_error={}", + path.display(), + set_named_result.0 + ); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::{ + current_process_user_sid_string, set_path_permission, + set_path_permission_for_portable_service_shmem_dir, + set_path_permission_for_portable_service_shmem_file, sid_string_to_local_alloc_guard, + LocalAllocGuard, ResultType, + }; + use hbb_common::bail; + use std::{ + fs, + os::windows::{ffi::OsStrExt, fs::symlink_dir, fs::symlink_file}, + path::{Path, PathBuf}, + }; + use windows::{ + core::PCWSTR, + Win32::{ + Security::{ + AclSizeInformation, + Authorization::{GetNamedSecurityInfoW, SE_FILE_OBJECT}, + EqualSid as WinEqualSid, GetAce, GetAclInformation, GetSecurityDescriptorControl, + ACCESS_ALLOWED_ACE, ACE_HEADER, ACL, ACL_SIZE_INFORMATION, + DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, PSID, SE_DACL_PROTECTED, + }, + Storage::FileSystem::{ + FILE_ALL_ACCESS, FILE_GENERIC_EXECUTE, FILE_GENERIC_READ, FILE_GENERIC_WRITE, + }, + }, + }; + + const ACCESS_ALLOWED_ACE_TYPE_U8: u8 = 0; + + fn unique_acl_test_path(prefix: &str) -> PathBuf { + std::env::temp_dir().join(format!( + "rustdesk_acl_{}_{}_{}", + prefix, + std::process::id(), + hbb_common::rand::random::() + )) + } + + fn try_create_dir_reparse_point(target: &Path, link: &Path, test_name: &str) -> bool { + match symlink_dir(target, link) { + Ok(()) => true, + Err(err) => { + eprintln!( + "skip {}: failed to create directory reparse point (symlink): {}", + test_name, err + ); + false + } + } + } + + fn try_create_file_reparse_point(target: &Path, link: &Path, test_name: &str) -> bool { + match symlink_file(target, link) { + Ok(()) => true, + Err(err) => { + eprintln!( + "skip {}: failed to create file reparse point (symlink): {}", + test_name, err + ); + false + } + } + } + + fn get_file_dacl(path: &Path) -> ResultType<(*mut ACL, LocalAllocGuard)> { + let mut dacl: *mut ACL = std::ptr::null_mut(); + let mut sd = PSECURITY_DESCRIPTOR::default(); + let path_utf16: Vec = path + .as_os_str() + .encode_wide() + .chain(std::iter::once(0)) + .collect(); + let result = unsafe { + GetNamedSecurityInfoW( + PCWSTR::from_raw(path_utf16.as_ptr()), + SE_FILE_OBJECT, + DACL_SECURITY_INFORMATION, + None, + None, + Some(&mut dacl), + None, + &mut sd, + ) + }; + if result.0 != 0 { + bail!( + "GetNamedSecurityInfoW failed for '{}': win32_error={}", + path.display(), + result.0 + ); + } + if dacl.is_null() || sd.0.is_null() { + bail!("DACL/security descriptor missing for '{}'", path.display()); + } + Ok((dacl, LocalAllocGuard(sd.0))) + } + + fn has_allow_ace_with_mask( + dacl: *const ACL, + sid_ptr: *mut std::ffi::c_void, + mask: u32, + ) -> bool { + let mut info = ACL_SIZE_INFORMATION::default(); + if unsafe { + GetAclInformation( + dacl, + &mut info as *mut _ as *mut std::ffi::c_void, + std::mem::size_of::() as u32, + AclSizeInformation, + ) + } + .is_err() + { + return false; + } + for index in 0..info.AceCount { + let mut ace_ptr: *mut std::ffi::c_void = std::ptr::null_mut(); + if unsafe { GetAce(dacl, index, &mut ace_ptr) }.is_err() || ace_ptr.is_null() { + continue; + } + let header = unsafe { &*(ace_ptr as *const ACE_HEADER) }; + if header.AceType != ACCESS_ALLOWED_ACE_TYPE_U8 { + continue; + } + let allowed = unsafe { &*(ace_ptr as *const ACCESS_ALLOWED_ACE) }; + let ace_sid = PSID((&allowed.SidStart as *const u32) as *mut std::ffi::c_void); + if unsafe { WinEqualSid(PSID(sid_ptr), ace_sid) }.is_ok() + && (allowed.Mask & mask) == mask + { + return true; + } + } + false + } + + fn has_any_allow_ace_for_sid(dacl: *const ACL, sid_ptr: *mut std::ffi::c_void) -> bool { + has_allow_ace_with_mask(dacl, sid_ptr, 0) + } + + fn is_dacl_protected(sd: PSECURITY_DESCRIPTOR) -> bool { + let mut control: u16 = 0; + let mut revision: u32 = 0; + if unsafe { GetSecurityDescriptorControl(sd, &mut control, &mut revision) }.is_err() { + return false; + } + (control & SE_DACL_PROTECTED.0) != 0 + } + + #[test] + fn test_portable_service_shmem_dir_acl_policy() { + let dir = unique_acl_test_path("dir"); + fs::create_dir_all(&dir).unwrap(); + set_path_permission_for_portable_service_shmem_dir(&dir).unwrap(); + + let (dacl, sd_guard) = get_file_dacl(&dir).unwrap(); + let current_user_sid = + sid_string_to_local_alloc_guard(¤t_process_user_sid_string().unwrap()).unwrap(); + let system_sid = sid_string_to_local_alloc_guard("S-1-5-18").unwrap(); + let admin_sid = sid_string_to_local_alloc_guard("S-1-5-32-544").unwrap(); + let auth_users_sid = sid_string_to_local_alloc_guard("S-1-5-11").unwrap(); + let everyone_sid = sid_string_to_local_alloc_guard("S-1-1-0").unwrap(); + let users_sid = sid_string_to_local_alloc_guard("S-1-5-32-545").unwrap(); + + assert!(has_allow_ace_with_mask( + dacl, + system_sid.as_sid_ptr(), + FILE_ALL_ACCESS.0 + )); + assert!(has_allow_ace_with_mask( + dacl, + admin_sid.as_sid_ptr(), + FILE_ALL_ACCESS.0 + )); + assert!(has_allow_ace_with_mask( + dacl, + current_user_sid.as_sid_ptr(), + FILE_ALL_ACCESS.0 + )); + assert!(has_allow_ace_with_mask( + dacl, + auth_users_sid.as_sid_ptr(), + FILE_GENERIC_WRITE.0 + )); + assert!(!has_any_allow_ace_for_sid(dacl, everyone_sid.as_sid_ptr())); + assert!(!has_any_allow_ace_for_sid(dacl, users_sid.as_sid_ptr())); + assert!(is_dacl_protected(PSECURITY_DESCRIPTOR( + sd_guard.as_sid_ptr() + ))); + + let _ = fs::remove_dir_all(&dir); + } + + #[test] + fn test_portable_service_shmem_file_acl_policy() { + let dir = unique_acl_test_path("file"); + fs::create_dir_all(&dir).unwrap(); + let file = dir.join("shared_memory_portable_service_test"); + fs::write(&file, b"x").unwrap(); + set_path_permission_for_portable_service_shmem_file(&file).unwrap(); + + let (dacl, sd_guard) = get_file_dacl(&file).unwrap(); + let current_user_sid = + sid_string_to_local_alloc_guard(¤t_process_user_sid_string().unwrap()).unwrap(); + let system_sid = sid_string_to_local_alloc_guard("S-1-5-18").unwrap(); + let admin_sid = sid_string_to_local_alloc_guard("S-1-5-32-544").unwrap(); + let auth_users_sid = sid_string_to_local_alloc_guard("S-1-5-11").unwrap(); + let everyone_sid = sid_string_to_local_alloc_guard("S-1-1-0").unwrap(); + let users_sid = sid_string_to_local_alloc_guard("S-1-5-32-545").unwrap(); + + assert!(has_allow_ace_with_mask( + dacl, + system_sid.as_sid_ptr(), + FILE_ALL_ACCESS.0 + )); + assert!(has_allow_ace_with_mask( + dacl, + admin_sid.as_sid_ptr(), + FILE_ALL_ACCESS.0 + )); + assert!(has_allow_ace_with_mask( + dacl, + current_user_sid.as_sid_ptr(), + FILE_ALL_ACCESS.0 + )); + assert!(!has_any_allow_ace_for_sid( + dacl, + auth_users_sid.as_sid_ptr() + )); + assert!(!has_any_allow_ace_for_sid(dacl, everyone_sid.as_sid_ptr())); + assert!(!has_any_allow_ace_for_sid(dacl, users_sid.as_sid_ptr())); + assert!(is_dacl_protected(PSECURITY_DESCRIPTOR( + sd_guard.as_sid_ptr() + ))); + + let _ = fs::remove_file(&file); + let _ = fs::remove_dir_all(&dir); + } + + #[test] + fn test_set_path_permission_rx_applies_recursively() { + let root = unique_acl_test_path("set_path_permission"); + let child_dir = root.join("child"); + let child_file = child_dir.join("helper.exe"); + fs::create_dir_all(&child_dir).unwrap(); + fs::write(&child_file, b"x").unwrap(); + + if let Err(err) = set_path_permission(&root, FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0) { + let text = err.to_string(); + let _ = fs::remove_file(&child_file); + let _ = fs::remove_dir_all(&root); + if text.contains("win32_error=5") || text.contains("Access is denied") { + eprintln!( + "skip test_set_path_permission_rx_applies_recursively: insufficient WRITE_DAC in current environment: {}", + text + ); + return; + } + panic!("set_path_permission failed unexpectedly: {}", text); + } + + let everyone_sid = sid_string_to_local_alloc_guard("S-1-1-0").unwrap(); + let rx_mask = FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0; + for target in [&root, &child_dir, &child_file] { + let (dacl, _sd_guard) = get_file_dacl(target).unwrap(); + assert!( + has_allow_ace_with_mask(dacl, everyone_sid.as_sid_ptr(), rx_mask), + "Everyone RX grant missing on '{}'", + target.display() + ); + } + + let _ = fs::remove_file(&child_file); + let _ = fs::remove_dir_all(&root); + } + + #[test] + fn test_portable_service_shmem_dir_acl_rejects_file_target() { + let dir = unique_acl_test_path("dir_target_file"); + fs::create_dir_all(&dir).unwrap(); + let file = dir.join("target.txt"); + fs::write(&file, b"x").unwrap(); + let result = set_path_permission_for_portable_service_shmem_dir(&file); + assert!(result.is_err()); + let _ = fs::remove_file(&file); + let _ = fs::remove_dir_all(&dir); + } + + #[test] + fn test_portable_service_shmem_file_acl_rejects_dir_target() { + let dir = unique_acl_test_path("file_target_dir"); + fs::create_dir_all(&dir).unwrap(); + let result = set_path_permission_for_portable_service_shmem_file(&dir); + assert!(result.is_err()); + let _ = fs::remove_dir_all(&dir); + } + + #[test] + fn test_portable_service_shmem_file_acl_rejects_missing_target() { + let path = unique_acl_test_path("missing").join("shared_memory_missing"); + let result = set_path_permission_for_portable_service_shmem_file(&path); + assert!(result.is_err()); + } + + #[test] + fn test_set_path_permission_rejects_reparse_entrypoint() { + let root = unique_acl_test_path("reparse_entry"); + let real_dir = root.join("real"); + let link_dir = root.join("link"); + fs::create_dir_all(&real_dir).unwrap(); + if !try_create_dir_reparse_point( + &real_dir, + &link_dir, + "test_set_path_permission_rejects_reparse_entrypoint", + ) { + let _ = fs::remove_dir_all(&real_dir); + let _ = fs::remove_dir_all(&root); + return; + } + + let result = set_path_permission(&link_dir, FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0); + let text = result.err().map(|e| e.to_string()).unwrap_or_default(); + assert!( + text.contains("reparse point"), + "expected reparse-point rejection, got '{}'", + text + ); + + let _ = fs::remove_dir(&link_dir); + let _ = fs::remove_dir_all(&real_dir); + let _ = fs::remove_dir_all(&root); + } + + #[test] + fn test_portable_service_shmem_dir_acl_rejects_reparse_target() { + let root = unique_acl_test_path("reparse_shmem_dir"); + let real_dir = root.join("real"); + let link_dir = root.join("link"); + fs::create_dir_all(&real_dir).unwrap(); + if !try_create_dir_reparse_point( + &real_dir, + &link_dir, + "test_portable_service_shmem_dir_acl_rejects_reparse_target", + ) { + let _ = fs::remove_dir_all(&real_dir); + let _ = fs::remove_dir_all(&root); + return; + } + + let result = set_path_permission_for_portable_service_shmem_dir(&link_dir); + let text = result.err().map(|e| e.to_string()).unwrap_or_default(); + assert!( + text.contains("reparse point"), + "expected reparse-point rejection, got '{}'", + text + ); + + let _ = fs::remove_dir(&link_dir); + let _ = fs::remove_dir_all(&real_dir); + let _ = fs::remove_dir_all(&root); + } + + #[test] + fn test_portable_service_shmem_file_acl_rejects_reparse_target() { + let root = unique_acl_test_path("reparse_shmem_file"); + let real_file = root.join("real.txt"); + let link_file = root.join("link.txt"); + fs::create_dir_all(&root).unwrap(); + fs::write(&real_file, b"x").unwrap(); + if !try_create_file_reparse_point( + &real_file, + &link_file, + "test_portable_service_shmem_file_acl_rejects_reparse_target", + ) { + let _ = fs::remove_file(&real_file); + let _ = fs::remove_dir_all(&root); + return; + } + + let result = set_path_permission_for_portable_service_shmem_file(&link_file); + let text = result.err().map(|e| e.to_string()).unwrap_or_default(); + assert!( + text.contains("reparse point"), + "expected reparse-point rejection, got '{}'", + text + ); + + let _ = fs::remove_file(&link_file); + let _ = fs::remove_file(&real_file); + let _ = fs::remove_dir_all(&root); + } +} diff --git a/src/server.rs b/src/server.rs index dddc762bf..e11003faa 100644 --- a/src/server.rs +++ b/src/server.rs @@ -731,7 +731,7 @@ async fn sync_and_watch_config_dir(sync_done_tx: Option { if !synced { if conn.send(&Data::SyncConfig(None)).await.is_ok() { @@ -772,6 +772,12 @@ async fn sync_and_watch_config_dir(sync_done_tx: Option { log::error!("sync config to root failed: {}", e); - match crate::ipc::connect(1000, "_service").await { + match crate::ipc::connect_service(1000).await { Ok(mut _conn) => { conn = _conn; log::info!("reconnected to ipc_service"); diff --git a/src/server/connection.rs b/src/server/connection.rs index a960daac1..f5019e447 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -22,8 +22,6 @@ use crate::{ #[cfg(any(target_os = "android", target_os = "ios"))] use crate::{common::DEVICE_NAME, flutter::connection_manager::start_channel}; use cidr_utils::cidr::IpCidr; -#[cfg(target_os = "linux")] -use hbb_common::platform::linux::run_cmds; #[cfg(target_os = "android")] use hbb_common::protobuf::EnumOrUnknown; use hbb_common::{ @@ -4983,6 +4981,9 @@ pub fn remove_pending_switch_sides_uuid(id: &str, uuid: &uuid::Uuid) -> bool { } #[cfg(not(any(target_os = "android", target_os = "ios")))] +// IPC bootstrap summary: +// - Resolve target CM socket (headless/non-headless, optional UID-scoped path on Linux). +// - Start CM when missing, then bridge bidirectional messages between this task and CM IPC. async fn start_ipc( mut rx_to_cm: mpsc::UnboundedReceiver, tx_from_cm: mpsc::UnboundedSender, @@ -4997,10 +4998,19 @@ async fn start_ipc( } sleep(1.).await; } + #[cfg(target_os = "linux")] + let headless_cm = crate::is_server() + && crate::platform::is_headless_allowed() + && linux_desktop_manager::is_headless(); + #[cfg(not(target_os = "linux"))] + let headless_cm = false; let mut stream = None; - if let Ok(s) = crate::ipc::connect(1000, "_cm").await { - stream = Some(s); - } else { + if !headless_cm { + if let Ok(s) = crate::ipc::connect(1000, "_cm").await { + stream = Some(s); + } + } + if stream.is_none() { #[allow(unused_mut)] #[allow(unused_assignments)] let mut args = vec!["--cm"]; @@ -5010,75 +5020,123 @@ async fn start_ipc( // Cm run as user, wait until desktop session is ready. #[cfg(target_os = "linux")] - if crate::platform::is_headless_allowed() && linux_desktop_manager::is_headless() { + if headless_cm { let mut username = linux_desktop_manager::get_username(); loop { if !username.is_empty() { break; } + // `_rx_desktop_ready` is used as a wake-up signal from desktop/session state changes + // (for example wait_desktop_cm_ready paths). It is not itself a proof of CM readiness. + // TODO: + // When `_rx_desktop_ready` is closed, `recv()` returns + // `None` immediately and this loop may spin if `username` remains empty. + // Keep behavior unchanged for now; if field reports appear, handle `Ok(None)` by + // breaking/returning to avoid hot-looping. let _res = timeout(1_000, _rx_desktop_ready.recv()).await; username = linux_desktop_manager::get_username(); } let uid = { - let output = run_cmds(&format!("id -u {}", &username))?; + let username_for_cmd = username.clone(); + let mut uid_cmd = hbb_common::tokio::process::Command::new("id"); + // TODO: + // Keep current behavior for now to minimize change risk. + // If usernames starting with '-' are observed in the field, prefer: + // `id -u -- ` to avoid option-parsing ambiguity. + // Already verified that `id -u -- ` works as expected on macOS and Ubuntu 24.04. + uid_cmd.arg("-u").arg(&username_for_cmd).kill_on_drop(true); + let output = timeout(10_000, uid_cmd.output()) + .await + .map_err(|_| anyhow!("Timed out querying uid for {}", username))? + .map_err(|e| anyhow!("Failed to run `id -u {}`: {}", username, e))?; + if !output.status.success() { + bail!("Failed to query uid for {}", username); + } + let output = String::from_utf8_lossy(&output.stdout); let output = output.trim(); - if output.is_empty() || !output.parse::().is_ok() { - bail!("Invalid username {}", &username); + if output.parse::().is_err() { + bail!("Invalid uid {}", output); } output.to_string() }; user = Some((uid, username)); args = vec!["--cm-no-ui"]; } - let run_done; - if crate::platform::is_root() { - let mut res = Ok(None); - for _ in 0..10 { - #[cfg(not(any(target_os = "linux")))] - { - log::debug!("Start cm"); - res = crate::platform::run_as_user(args.clone()); - } - #[cfg(target_os = "linux")] - { - log::debug!("Start cm"); - res = crate::platform::run_as_user( - args.clone(), - user.clone(), - None::<(&str, &str)>, - ); - } - if res.is_ok() { - break; - } - log::error!("Failed to run cm: {res:?}"); - sleep(1.).await; - } - if let Some(task) = res? { - super::CHILD_PROCESS.lock().unwrap().push(task); - } - run_done = true; - } else { - run_done = false; - } - if !run_done { - log::debug!("Start cm"); - super::CHILD_PROCESS - .lock() - .unwrap() - .push(crate::run_me(args)?); - } - for _ in 0..20 { - sleep(0.3).await; - if let Ok(s) = crate::ipc::connect(1000, "_cm").await { + #[cfg(target_os = "linux")] + let cm_uid: Option = match &user { + Some((uid, _)) => Some( + uid.parse::() + .map_err(|_| anyhow!("Invalid uid {}", uid))?, + ), + None => None, + }; + #[cfg(target_os = "linux")] + if let Some(uid) = cm_uid { + if let Ok(s) = crate::ipc::connect_for_uid(1000, uid, "_cm").await { stream = Some(s); - break; } } if stream.is_none() { - bail!("Failed to connect to connection manager"); + let run_done; + if crate::platform::is_root() { + let mut res = Ok(None); + for _ in 0..10 { + #[cfg(not(any(target_os = "linux")))] + { + log::debug!("Start cm"); + res = crate::platform::run_as_user(args.clone()); + } + #[cfg(target_os = "linux")] + { + log::debug!("Start cm"); + res = crate::platform::run_as_user( + args.clone(), + user.clone(), + None::<(&str, &str)>, + ); + } + if res.is_ok() { + break; + } + log::error!("Failed to run cm: {res:?}"); + sleep(1.).await; + } + if let Some(task) = res? { + super::CHILD_PROCESS.lock().unwrap().push(task); + } + run_done = true; + } else { + run_done = false; + } + if !run_done { + log::debug!("Start cm"); + super::CHILD_PROCESS + .lock() + .unwrap() + .push(crate::run_me(args)?); + } + for _ in 0..20 { + sleep(0.3).await; + #[cfg(target_os = "linux")] + { + if let Some(uid) = cm_uid { + if let Ok(s) = crate::ipc::connect_for_uid(1000, uid, "_cm").await { + stream = Some(s); + break; + } + continue; + } + } + if let Ok(s) = crate::ipc::connect(1000, "_cm").await { + stream = Some(s); + break; + } + } } } + if stream.is_none() { + bail!("Failed to connect to connection manager"); + } let _res = tx_stream_ready.send(()).await; let mut stream = stream.ok_or(anyhow!("none stream"))?; diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index 6f5695046..23b69a70c 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -1,3 +1,11 @@ +use crate::{ + ipc::{self, new_listener, Connection, Data, DataPortableService, IPC_TOKEN_LEN}, + platform::{ + set_path_permission, set_path_permission_for_portable_service_shmem_dir, + set_path_permission_for_portable_service_shmem_file, + validate_path_for_portable_service_shmem_dir, + }, +}; use core::slice; use hbb_common::{ allow_err, @@ -15,26 +23,26 @@ use shared_memory::*; use std::{ mem::size_of, ops::{Deref, DerefMut}, - path::Path, - sync::{Arc, Mutex}, + path::{Path, PathBuf}, + sync::{ + atomic::{AtomicBool, AtomicU64, Ordering}, + Arc, Mutex, + }, time::Duration, }; use winapi::{ shared::minwindef::{BOOL, FALSE, TRUE}, um::winuser::{self, CURSORINFO, PCURSORINFO}, }; - -use crate::{ - ipc::{self, new_listener, Connection, Data, DataPortableService}, - platform::set_path_permission, -}; +use windows::Win32::Storage::FileSystem::{FILE_GENERIC_EXECUTE, FILE_GENERIC_READ}; use super::video_qos; const SIZE_COUNTER: usize = size_of::() * 2; const FRAME_ALIGN: usize = 64; -const ADDR_CURSOR_PARA: usize = 0; +const ADDR_IPC_TOKEN: usize = 0; +const ADDR_CURSOR_PARA: usize = ADDR_IPC_TOKEN + IPC_TOKEN_LEN; const ADDR_CURSOR_COUNTER: usize = ADDR_CURSOR_PARA + size_of::(); const ADDR_CAPTURER_PARA: usize = ADDR_CURSOR_COUNTER + SIZE_COUNTER; @@ -44,12 +52,186 @@ const ADDR_CAPTURE_FRAME_COUNTER: usize = ADDR_CAPTURE_WOULDBLOCK + size_of:: bool { + !name.is_empty() + && name.len() <= SHMEM_NAME_MAX_LEN + && name + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'-') +} + +#[inline] +pub fn portable_service_shmem_arg(name: &str) -> String { + format!("{SHMEM_ARG_PREFIX}{name}") +} + +#[inline] +fn is_valid_portable_service_ipc_token(token: &str) -> bool { + token.len() == IPC_TOKEN_LEN + && token + .bytes() + .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()) +} + +#[inline] +fn read_ipc_token_from_shmem(shmem: &SharedMemory) -> Option { + if shmem.len() < ADDR_IPC_TOKEN + IPC_TOKEN_LEN { + log::error!( + "Portable service shared memory too small: len={}, need>={}", + shmem.len(), + ADDR_IPC_TOKEN + IPC_TOKEN_LEN + ); + return None; + } + unsafe { + let ptr = shmem.as_ptr().add(ADDR_IPC_TOKEN); + let bytes = slice::from_raw_parts(ptr, IPC_TOKEN_LEN); + let end = bytes + .iter() + .position(|byte| *byte == 0) + .unwrap_or(IPC_TOKEN_LEN); + if end == 0 { + return None; + } + let token = std::str::from_utf8(&bytes[..end]).ok()?.to_owned(); + if is_valid_portable_service_ipc_token(&token) { + Some(token) + } else { + None + } + } +} + +#[inline] +fn validate_runtime_shmem_layout(shmem: &SharedMemory) -> ResultType<()> { + if shmem.len() < MIN_RUNTIME_SHMEM_LEN { + bail!( + "Portable service shared memory too small for runtime layout: len={}, need>={}", + shmem.len(), + MIN_RUNTIME_SHMEM_LEN + ); + } + Ok(()) +} + +#[inline] +fn is_valid_capture_frame_length(shmem_len: usize, frame_len: usize) -> bool { + let frame_capacity = shmem_len.saturating_sub(ADDR_CAPTURE_FRAME); + frame_len > 0 && frame_len <= frame_capacity +} + +#[inline] +fn shared_memory_flink_path_by_name(name: &str) -> ResultType { + let mut dir = crate::platform::user_accessible_folder()?; + dir = dir.join(hbb_common::config::APP_NAME.read().unwrap().clone()); + dir = dir.join(SHMEM_PARENT_DIR); + Ok(dir.join(format!("shared_memory{}", name))) +} + +#[inline] +fn remove_shared_memory_flink_once(name: &str, log_on_error: bool, log_context: &str) -> bool { + let flink = match shared_memory_flink_path_by_name(name) { + Ok(path) => path, + Err(err) => { + if log_on_error { + log::warn!( + "{} failed to resolve portable service shared-memory flink path for '{}': {}", + log_context, + name, + err + ); + } + return false; + } + }; + match std::fs::remove_file(&flink) { + Ok(()) => { + log::info!( + "{} removed portable service shared-memory flink artifact: {:?}", + log_context, + flink + ); + true + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => true, + Err(err) => { + if log_on_error { + log::warn!( + "{} failed to remove portable service shared-memory flink artifact {:?}: {}", + log_context, + flink, + err + ); + } + false + } + } +} + +#[inline] +fn write_ipc_token_to_shmem(shmem: &SharedMemory, token: &str) -> ResultType<()> { + if !is_valid_portable_service_ipc_token(token) { + bail!("Invalid portable service ipc token"); + } + shmem.write(ADDR_IPC_TOKEN, token.as_bytes()); + Ok(()) +} + +#[inline] +fn clear_ipc_token_in_shmem(shmem: &SharedMemory) { + shmem.write(ADDR_IPC_TOKEN, &[0u8; IPC_TOKEN_LEN]); +} + +#[inline] +fn portable_service_arg_value_candidate_from_arg<'a>( + arg: &'a str, + prefix: &str, +) -> Option<&'a str> { + let mut value = arg.strip_prefix(prefix)?; + value = value.trim_start(); + value = value + .strip_prefix('"') + .or_else(|| value.strip_prefix('\'')) + .unwrap_or(value); + value = value.split_whitespace().next().unwrap_or_default(); + value = value.trim_matches(|c| c == '"' || c == '\''); + Some(value) +} + +#[inline] +pub fn portable_service_shmem_name_from_args() -> Option { + for arg in std::env::args() { + if let Some(value) = portable_service_arg_value_candidate_from_arg(&arg, SHMEM_ARG_PREFIX) { + if is_valid_portable_service_shmem_name(value) { + return Some(value.to_owned()); + } + log::error!( + "Invalid portable service shared memory name argument: '{}'", + value + ); + return None; + } + } + None +} + +#[inline] +pub fn has_portable_service_shmem_arg() -> bool { + std::env::args().any(|arg| arg.starts_with(SHMEM_ARG_PREFIX)) +} + pub struct SharedMemory { inner: Shmem, } @@ -92,7 +274,27 @@ impl SharedMemory { } }; log::info!("Create shared memory, size: {}, flink: {}", size, flink); - set_path_permission(Path::new(&flink), "F").ok(); + if let Err(err) = set_path_permission_for_portable_service_shmem_file(Path::new(&flink)) { + // Release shmem handle first so best-effort flink cleanup has a chance to succeed. + drop(shmem); + match std::fs::remove_file(&flink) { + Ok(()) => { + log::info!( + "Create cleanup removed portable service shared-memory flink artifact: {}", + flink + ); + } + Err(remove_err) if remove_err.kind() == std::io::ErrorKind::NotFound => {} + Err(remove_err) => { + log::warn!( + "Create cleanup failed to remove portable service shared-memory flink artifact {}: {}", + flink, + remove_err + ); + } + } + return Err(err); + } Ok(SharedMemory { inner: shmem }) } @@ -120,9 +322,18 @@ impl SharedMemory { fn flink(name: String) -> ResultType { let mut dir = crate::platform::user_accessible_folder()?; dir = dir.join(hbb_common::config::APP_NAME.read().unwrap().clone()); - if !dir.exists() { - std::fs::create_dir(&dir)?; - set_path_permission(&dir, "F").ok(); + dir = dir.join(SHMEM_PARENT_DIR); + let parent_created = !dir.exists(); + if parent_created { + std::fs::create_dir_all(&dir)?; + } + if parent_created || crate::platform::is_root() { + // Harden parent ACL on first provisioning and periodically on SYSTEM path. + set_path_permission_for_portable_service_shmem_dir(&dir)?; + } else { + // Existing parents still need type/reparse validation. Non-SYSTEM callers may lack + // WRITE_DAC on a valid parent, so avoid rebuilding the ACL here. + validate_path_for_portable_service_shmem_dir(&dir)?; } Ok(dir .join(format!("shared_memory{}", name)) @@ -232,16 +443,45 @@ pub mod server { lazy_static::lazy_static! { static ref EXIT: Arc> = Default::default(); + static ref FORCE_EXIT_ARMED: AtomicBool = AtomicBool::new(false); } pub fn run_portable_service() { - let shmem = match SharedMemory::open_existing(SHMEM_NAME) { + let shmem_name = match portable_service_shmem_name_from_args() { + Some(name) => name, + None => { + if has_portable_service_shmem_arg() { + log::error!( + "Invalid portable service shared memory argument, aborting startup" + ); + } else { + log::error!( + "Missing portable service shared memory argument, aborting startup" + ); + } + return; + } + }; + let shmem = match SharedMemory::open_existing(&shmem_name) { Ok(shmem) => Arc::new(shmem), Err(e) => { log::error!("Failed to open existing shared memory: {:?}", e); return; } }; + if let Err(e) = validate_runtime_shmem_layout(shmem.as_ref()) { + log::error!("{}", e); + return; + } + let ipc_token = match read_ipc_token_from_shmem(shmem.as_ref()) { + Some(token) => token, + None => { + log::error!( + "Missing portable service ipc token in shared memory, aborting startup" + ); + return; + } + }; let shmem1 = shmem.clone(); let shmem2 = shmem.clone(); let mut threads = vec![]; @@ -251,17 +491,24 @@ pub mod server { threads.push(std::thread::spawn(|| { run_capture(shmem2); })); - threads.push(std::thread::spawn(|| { - run_ipc_client(); + threads.push(std::thread::spawn(move || { + run_ipc_client(ipc_token); })); - threads.push(std::thread::spawn(|| { + // Detached shutdown watchdog: + // - gives graceful shutdown/cleanup a short window + // - force-exits the process if workers are still stuck + std::thread::spawn(|| { run_exit_check(); - })); + }); let record_pos_handle = crate::input_service::try_start_record_cursor_pos(); + // Arm forced-exit watchdog only for worker join phase. + // Once join phase completes, cleanup should not be interrupted by forced exit. + FORCE_EXIT_ARMED.store(true, Ordering::SeqCst); for th in threads.drain(..) { th.join().ok(); log::info!("thread joined"); } + FORCE_EXIT_ARMED.store(false, Ordering::SeqCst); crate::input_service::try_stop_record_cursor_pos(); if let Some(handle) = record_pos_handle { @@ -270,16 +517,47 @@ pub mod server { Err(e) => log::error!("record_pos_handle join error {:?}", &e), } } + drop(shmem); + remove_shared_memory_flink_with_retry(&shmem_name); } fn run_exit_check() { + const FORCED_EXIT_DELAY: Duration = Duration::from_secs(3); loop { if EXIT.lock().unwrap().clone() { - std::thread::sleep(Duration::from_millis(50)); - std::process::exit(0); + break; } std::thread::sleep(Duration::from_millis(50)); } + // Fallback only: normal shutdown path should complete and process should exit naturally. + // This forced exit is a last resort when worker threads are stuck and graceful teardown + // does not finish in time. + std::thread::sleep(FORCED_EXIT_DELAY); + if FORCE_EXIT_ARMED.load(Ordering::SeqCst) { + log::warn!( + "Portable service shutdown watchdog fallback triggered: forcing process exit after {:?}", + FORCED_EXIT_DELAY + ); + std::process::exit(0); + } + } + + fn remove_shared_memory_flink_with_retry(name: &str) { + const MAX_RETRY: usize = 20; + const RETRY_INTERVAL: Duration = Duration::from_millis(200); + for attempt in 0..MAX_RETRY { + let is_last_attempt = attempt + 1 == MAX_RETRY; + if remove_shared_memory_flink_once(name, is_last_attempt, "SYSTEM cleanup") { + return; + } + if !is_last_attempt { + std::thread::sleep(RETRY_INTERVAL); + } + } + log::warn!( + "SYSTEM cleanup failed to remove portable service shared-memory flink artifact '{}' after retry", + name + ); } fn run_get_cursor_info(shmem: Arc) { @@ -386,6 +664,17 @@ pub mod server { match c.as_mut().map(|f| f.frame(spf)) { Some(Ok(f)) => match f { Frame::PixelBuffer(f) => { + let frame_capacity = shmem.len().saturating_sub(ADDR_CAPTURE_FRAME); + if f.data().len() > frame_capacity { + log::error!( + "Portable service capture frame exceeds shared memory capacity: frame_len={}, capacity={}, shmem_len={}", + f.data().len(), + frame_capacity, + shmem.len() + ); + *EXIT.lock().unwrap() = true; + return; + } utils::set_frame_info( &shmem, FrameInfo { @@ -436,17 +725,33 @@ pub mod server { } #[tokio::main(flavor = "current_thread")] - async fn run_ipc_client() { + async fn run_ipc_client(ipc_token: String) { use DataPortableService::*; let postfix = IPC_SUFFIX; match ipc::connect(1000, postfix).await { Ok(mut stream) => { + if let Err(err) = + ipc::portable_service_ipc_handshake_as_client(&mut stream, &ipc_token).await + { + log::error!("portable service ipc handshake failed: {}", err); + *EXIT.lock().unwrap() = true; + return; + } let mut timer = crate::rustdesk_interval(tokio::time::interval(Duration::from_secs(1))); let mut nack = 0; loop { + if *EXIT.lock().unwrap() { + log::info!("Portable service EXIT signaled, closing ipc client loop"); + stream + .send(&Data::DataPortableService(WillClose)) + .await + .ok(); + break; + } + tokio::select! { res = stream.next() => { match res { @@ -526,7 +831,11 @@ pub mod client { lazy_static::lazy_static! { static ref RUNNING: Arc> = Default::default(); + static ref STARTING: Arc> = Default::default(); + static ref STARTING_TOKEN: AtomicU64 = AtomicU64::new(0); static ref SHMEM: Arc>> = Default::default(); + static ref SHMEM_RUNTIME_NAME: Arc>> = Default::default(); + static ref IPC_RUNTIME_TOKEN: Arc>> = Default::default(); static ref SENDER : Mutex> = Mutex::new(client::start_ipc_server()); static ref QUICK_SUPPORT: Arc> = Default::default(); } @@ -536,12 +845,176 @@ pub mod client { Logon(String, String), } + fn has_running_portable_service_process() -> bool { + let app_exe = format!("{}.exe", crate::get_app_name().to_lowercase()); + !crate::platform::get_pids_of_process_with_first_arg(&app_exe, "--portable-service") + .is_empty() + } + + #[inline] + fn next_portable_service_shmem_name() -> String { + format!( + "{}_{}_{:08x}", + crate::portable_service::SHMEM_NAME, + std::process::id(), + hbb_common::rand::random::() + ) + } + + #[inline] + fn set_runtime_ipc_token(token: String) { + *IPC_RUNTIME_TOKEN.lock().unwrap() = Some(token); + } + + #[inline] + fn schedule_remove_runtime_shmem_flink_retry(name: String) { + std::thread::spawn(move || { + const MAX_RETRY: usize = 20; + const RETRY_INTERVAL: Duration = Duration::from_millis(200); + for _ in 0..MAX_RETRY { + std::thread::sleep(RETRY_INTERVAL); + if remove_shared_memory_flink_once(&name, false, "Client cleanup") { + return; + } + } + log::warn!( + "Failed to remove portable service shared-memory flink artifact '{}' after retry", + name + ); + }); + } + + #[inline] + fn clear_runtime_shmem_state() { + let mut runtime_token = IPC_RUNTIME_TOKEN.lock().unwrap(); + let mut shmem_lock = SHMEM.lock().unwrap(); + if let Some(shmem) = shmem_lock.as_mut() { + clear_ipc_token_in_shmem(shmem); + } + *shmem_lock = None; + let runtime_name = SHMEM_RUNTIME_NAME.lock().unwrap().take(); + *runtime_token = None; + drop(runtime_token); + drop(shmem_lock); + if let Some(name) = runtime_name.as_deref() { + if !remove_shared_memory_flink_once(name, true, "Client cleanup") { + schedule_remove_runtime_shmem_flink_retry(name.to_owned()); + } + } + } + + #[inline] + fn consume_runtime_ipc_token_if_match(candidate: &str) -> (bool, Option) { + let mut token = IPC_RUNTIME_TOKEN.lock().unwrap(); + if !token + .as_deref() + .is_some_and(|expected| ipc::constant_time_ipc_token_eq(expected, candidate)) + { + return (false, None); + } + let mut shmem_lock = SHMEM.lock().unwrap(); + let matched_shmem_name = SHMEM_RUNTIME_NAME.lock().unwrap().clone(); + *token = None; + if let Some(shmem) = shmem_lock.as_mut() { + clear_ipc_token_in_shmem(shmem); + } + (true, matched_shmem_name) + } + + #[inline] + fn restore_runtime_ipc_token_after_failed_handshake( + token: &str, + expected_shmem_name: Option<&str>, + ) { + let mut runtime_token = IPC_RUNTIME_TOKEN.lock().unwrap(); + if let Some(current) = runtime_token.as_deref() { + if current != token { + log::debug!( + "Skip restoring portable service ipc token after handshake failure: runtime token has changed to a newer value" + ); + return; + } + } + let mut shmem_lock = SHMEM.lock().unwrap(); + let current_shmem_name = SHMEM_RUNTIME_NAME.lock().unwrap().clone(); + if current_shmem_name.as_deref() != expected_shmem_name { + if runtime_token.as_deref() == Some(token) { + *runtime_token = None; + } + log::debug!( + "Skip restoring portable service ipc token after handshake failure: shared-memory instance has changed" + ); + return; + } + let shmem_write_error = if let Some(shmem) = shmem_lock.as_mut() { + write_ipc_token_to_shmem(shmem, token) + .err() + .map(|err| err.to_string()) + } else { + Some("shared memory unavailable".to_owned()) + }; + if let Some(err) = shmem_write_error { + if runtime_token.as_deref() == Some(token) { + *runtime_token = None; + } + log::warn!( + "Failed to restore portable service ipc token after handshake failure: {}", + err + ); + return; + } + *runtime_token = Some(token.to_owned()); + } + + #[inline] + fn schedule_starting_timeout_reset(launch_token: u64) { + std::thread::spawn(move || { + std::thread::sleep(PORTABLE_SERVICE_STARTUP_TIMEOUT); + let should_reset = { + // Guard against stale watchdogs from previous launches: + // only the watchdog that matches the latest STARTING_TOKEN may reset STARTING. + let current_token = STARTING_TOKEN.load(Ordering::SeqCst); + // Keep lock guards in explicit short scopes to make it obvious + // there is no nested lock ordering (and to avoid Copilot false positives). + let starting = { *STARTING.lock().unwrap() }; + let running = { *RUNNING.lock().unwrap() }; + current_token == launch_token && starting && !running + }; + if should_reset { + log::warn!( + "Portable service startup timeout before IPC ready, reset STARTING state" + ); + *STARTING.lock().unwrap() = false; + } + }); + } + + // Launch flow summary: + // 1) Prepare/reset runtime shared memory + IPC token. + // 2) Start helper process (direct or logon) with shmem argument. + // 3) Keep STARTING=true until IPC ping/pong marks RUNNING, or timeout watchdog resets it. pub(crate) fn start_portable_service(para: StartPara) -> ResultType<()> { log::info!("start portable service"); - if RUNNING.lock().unwrap().clone() { - bail!("already running"); - } - if SHMEM.lock().unwrap().is_none() { + let launch_token = { + // Keep lock guards in explicit short scopes to make it obvious + // there is no nested lock ordering (and to avoid Copilot false positives). + let running = { *RUNNING.lock().unwrap() }; + let mut starting = STARTING.lock().unwrap(); + if *starting && !running && !has_running_portable_service_process() { + log::warn!( + "Detected stale portable service STARTING state without running process, reset it" + ); + *starting = false; + } + if *starting || running { + bail!("already running"); + } + *starting = true; + STARTING_TOKEN.fetch_add(1, Ordering::SeqCst) + 1 + }; + let start_result = (|| -> ResultType<()> { + clear_runtime_shmem_state(); + let mut shmem_lock = SHMEM.lock().unwrap(); let displays = scrap::Display::all()?; if displays.is_empty() { bail!("no display available!"); @@ -558,84 +1031,153 @@ pub mod client { } } } - let shmem_size = utils::align(ADDR_CAPTURE_FRAME + max_pixel * 4, align); + let shmem_size = + utils::align(ADDR_CAPTURE_FRAME + max_pixel * 4, align).max(MIN_RUNTIME_SHMEM_LEN); + let shmem_name = next_portable_service_shmem_name(); + if !is_valid_portable_service_shmem_name(&shmem_name) { + bail!("Generated invalid portable service shared memory name"); + } + let ipc_token = ipc::generate_one_time_ipc_token()?; // os error 112, no enough space - *SHMEM.lock().unwrap() = Some(crate::portable_service::SharedMemory::create( - crate::portable_service::SHMEM_NAME, + *shmem_lock = Some(crate::portable_service::SharedMemory::create( + &shmem_name, shmem_size, )?); + *SHMEM_RUNTIME_NAME.lock().unwrap() = Some(shmem_name); shutdown_hooks::add_shutdown_hook(drop_portable_service_shared_memory); - } - if let Some(shmem) = SHMEM.lock().unwrap().as_mut() { - unsafe { - libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _); - } - } - match para { - StartPara::Direct => { - if let Err(e) = crate::platform::run_background( - &std::env::current_exe()?.to_string_lossy().to_string(), - "--portable-service", - ) { - *SHMEM.lock().unwrap() = None; - bail!("Failed to run portable service process: {}", e); + let shmem_name = SHMEM_RUNTIME_NAME + .lock() + .unwrap() + .clone() + .ok_or_else(|| anyhow!("portable service shared memory name is unavailable"))?; + let init_token_result = if let Some(shmem) = shmem_lock.as_mut() { + unsafe { + libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _); } + write_ipc_token_to_shmem(shmem, &ipc_token) + } else { + Ok(()) + }; + if let Err(e) = init_token_result { + drop(shmem_lock); + clear_runtime_shmem_state(); + bail!( + "Failed to initialize portable service ipc token in shared memory: {}", + e + ); + }; + drop(shmem_lock); + set_runtime_ipc_token(ipc_token.clone()); + let portable_service_arg = format!( + "--portable-service {}", + crate::portable_service::portable_service_shmem_arg(&shmem_name) + ); + { + let _sender = SENDER.lock().unwrap(); } - StartPara::Logon(username, password) => { - #[allow(unused_mut)] - let mut exe = std::env::current_exe()?.to_string_lossy().to_string(); - #[cfg(feature = "flutter")] - { - if let Some(dir) = Path::new(&exe).parent() { - if set_path_permission(Path::new(dir), "RX").is_err() { - *SHMEM.lock().unwrap() = None; - bail!("Failed to set permission of {:?}", dir); + match para { + StartPara::Direct => { + match crate::platform::run_background( + &std::env::current_exe()?.to_string_lossy().to_string(), + &portable_service_arg, + ) { + Ok(true) => {} + Ok(false) => { + clear_runtime_shmem_state(); + bail!("Failed to run portable service process"); + } + Err(e) => { + clear_runtime_shmem_state(); + bail!("Failed to run portable service process: {}", e); } } } - #[cfg(not(feature = "flutter"))] - match hbb_common::directories_next::UserDirs::new() { - Some(user_dir) => { - let dir = user_dir - .home_dir() - .join("AppData") - .join("Local") - .join("rustdesk-sciter"); - if std::fs::create_dir_all(&dir).is_ok() { - let dst = dir.join("rustdesk.exe"); - if std::fs::copy(&exe, &dst).is_ok() { - if dst.exists() { - if set_path_permission(&dir, "RX").is_ok() { - exe = dst.to_string_lossy().to_string(); - } - } + StartPara::Logon(username, password) => { + #[allow(unused_mut)] + let mut exe = std::env::current_exe()?.to_string_lossy().to_string(); + #[cfg(feature = "flutter")] + { + if let Some(dir) = Path::new(&exe).parent() { + if let Err(err) = set_path_permission( + Path::new(dir), + FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0, + ) { + clear_runtime_shmem_state(); + bail!("Failed to set permission of {:?}: {}", dir, err); } } } - None => {} - } - if let Err(e) = crate::platform::windows::create_process_with_logon( - username.as_str(), - password.as_str(), - &exe, - "--portable-service", - ) { - *SHMEM.lock().unwrap() = None; - bail!("Failed to run portable service process: {}", e); + #[cfg(not(feature = "flutter"))] + if let Some((dir, dst)) = + crate::platform::windows::portable_service_logon_helper_paths() + { + let cleanup_helper_artifacts = || { + if Path::new(&exe) != dst { + std::fs::remove_file(&dst).ok(); + } + std::fs::remove_dir(&dir).ok(); + }; + let mut use_logon_helper_exe = false; + if let Err(err) = std::fs::create_dir_all(&dir) { + log::warn!( + "Failed to create portable service logon helper dir {:?}: {}", + dir, + err + ); + } else if let Err(err) = std::fs::copy(&exe, &dst) { + log::warn!( + "Failed to copy portable service logon helper binary from '{}' to {:?}: {}", + exe, + dst, + err + ); + cleanup_helper_artifacts(); + } else if !dst.exists() { + log::warn!( + "Portable service logon helper binary missing after copy: {:?}", + dst + ); + cleanup_helper_artifacts(); + } else if let Err(err) = + set_path_permission(&dir, FILE_GENERIC_READ.0 | FILE_GENERIC_EXECUTE.0) + { + log::warn!( + "Failed to set portable service logon helper path permission for {:?}: {}", + dir, + err + ); + cleanup_helper_artifacts(); + } else { + use_logon_helper_exe = true; + } + if use_logon_helper_exe { + exe = dst.to_string_lossy().to_string(); + } + } + if let Err(e) = crate::platform::windows::create_process_with_logon( + username.as_str(), + password.as_str(), + &exe, + &portable_service_arg, + ) { + clear_runtime_shmem_state(); + bail!("Failed to run portable service process: {}", e); + } } } + schedule_starting_timeout_reset(launch_token); + Ok(()) + })(); + if start_result.is_err() { + *STARTING.lock().unwrap() = false; } - let _sender = SENDER.lock().unwrap(); - Ok(()) + start_result } pub extern "C" fn drop_portable_service_shared_memory() { // https://stackoverflow.com/questions/35980148/why-does-an-atexit-handler-panic-when-it-accesses-stdout // Please make sure there is no print in the call stack - let mut lock = SHMEM.lock().unwrap(); - if lock.is_some() { - *lock = None; - } + clear_runtime_shmem_state(); } pub fn set_quick_support(v: bool) { @@ -655,7 +1197,11 @@ pub mod client { let mut option = SHMEM.lock().unwrap(); if let Some(shmem) = option.as_mut() { unsafe { - libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _); + libc::memset( + shmem.as_ptr().add(ADDR_CURSOR_PARA) as _, + 0, + shmem.len().saturating_sub(ADDR_CURSOR_PARA) as _, + ); } utils::set_para( shmem, @@ -702,6 +1248,19 @@ pub mod client { if utils::counter_ready(base.add(ADDR_CAPTURE_FRAME_COUNTER)) { let frame_info_ptr = shmem.as_ptr().add(ADDR_CAPTURE_FRAME_INFO); let frame_info = frame_info_ptr as *const FrameInfo; + let frame_len = (*frame_info).length; + if !is_valid_capture_frame_length(shmem.len(), frame_len) { + log::error!( + "Portable service frame length exceeds shared memory capacity: frame_len={}, shmem_len={}, frame_addr={}", + frame_len, + shmem.len(), + ADDR_CAPTURE_FRAME + ); + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "invalid portable service frame length".to_string(), + )); + } if (*frame_info).width != self.width || (*frame_info).height != self.height { log::info!( "skip frame, ({},{}) != ({},{})", @@ -716,7 +1275,7 @@ pub mod client { )); } let frame_ptr = base.add(ADDR_CAPTURE_FRAME); - let data = slice::from_raw_parts(frame_ptr, (*frame_info).length); + let data = slice::from_raw_parts(frame_ptr, frame_len); Ok(Frame::PixelBuffer(PixelBuffer::with_BGRA( data, self.width, @@ -778,10 +1337,49 @@ pub mod client { Some(result) = incoming.next() => { match result { Ok(stream) => { + let mut stream = Connection::new(stream); + if !ipc::authorize_windows_portable_service_ipc_connection( + &stream, postfix, + ) { + continue; + } + let mut consumed_token: Option = None; + let mut consumed_token_shmem_name: Option = None; + let handshake_result = + ipc::portable_service_ipc_handshake_as_server( + &mut stream, + |token| { + let (matched, matched_shmem_name) = + consume_runtime_ipc_token_if_match(token); + if matched { + consumed_token = Some(token.to_owned()); + consumed_token_shmem_name = matched_shmem_name; + true + } else { + false + } + }, + ) + .await; + if let Err(err) = handshake_result { + if let Some(token) = consumed_token.as_deref() { + restore_runtime_ipc_token_after_failed_handshake( + token, + consumed_token_shmem_name.as_deref(), + ); + *STARTING.lock().unwrap() = false; + } + log::warn!( + "Rejected portable service ipc connection due to token handshake failure: postfix={}, err={}", + postfix, + err + ); + continue; + } log::info!("Got portable service ipc connection"); let rx_clone = rx.clone(); tokio::spawn(async move { - let mut stream = Connection::new(stream); + let mut stream = stream; let postfix = postfix.to_owned(); let mut timer = crate::rustdesk_interval(tokio::time::interval(Duration::from_secs(1))); let mut nack = 0; @@ -805,6 +1403,7 @@ pub mod client { Pong => { nack = 0; *RUNNING.lock().unwrap() = true; + *STARTING.lock().unwrap() = false; }, ConnCount(None) => { if !quick_support { @@ -841,6 +1440,7 @@ pub mod client { } } *RUNNING.lock().unwrap() = false; + *STARTING.lock().unwrap() = false; }); } Err(err) => { @@ -990,3 +1590,23 @@ pub struct FrameInfo { width: usize, height: usize, } + +#[cfg(test)] +mod tests { + use super::{is_valid_capture_frame_length, ADDR_CAPTURE_FRAME}; + + #[test] + fn test_is_valid_capture_frame_length_rejects_zero_length() { + assert!(!is_valid_capture_frame_length(ADDR_CAPTURE_FRAME + 1024, 0)); + } + + #[test] + fn test_is_valid_capture_frame_length_rejects_out_of_bounds_length() { + assert!(!is_valid_capture_frame_length(ADDR_CAPTURE_FRAME + 16, 17)); + } + + #[test] + fn test_is_valid_capture_frame_length_accepts_in_bounds_length() { + assert!(is_valid_capture_frame_length(ADDR_CAPTURE_FRAME + 16, 16)); + } +} diff --git a/src/server/uinput.rs b/src/server/uinput.rs index a808b4aaa..a1947d79f 100644 --- a/src/server/uinput.rs +++ b/src/server/uinput.rs @@ -185,9 +185,13 @@ pub mod client { pub mod service { use super::*; use hbb_common::lazy_static; + #[cfg(target_os = "linux")] + use parity_tokio_ipc::Connection as RawIpcConnection; use scrap::wayland::{ pipewire::RDP_SESSION_INFO, remote_desktop_portal::OrgFreedesktopPortalRemoteDesktop, }; + #[cfg(target_os = "linux")] + use std::os::unix::io::AsRawFd; use std::{collections::HashMap, sync::Mutex}; lazy_static::lazy_static! { @@ -602,7 +606,10 @@ pub mod service { } DataKeyboard::KeyDown(enigo::Key::Raw(code)) => { if *code < 8 { - log::error!("Invalid Raw keycode {} (must be >= 8 due to XKB offset), skipping", code); + log::error!( + "Invalid Raw keycode {} (must be >= 8 due to XKB offset), skipping", + code + ); } else { let down_event = InputEvent::new(EventType::KEY, *code - 8, 1); allow_err!(keyboard.emit(&[down_event])); @@ -610,7 +617,10 @@ pub mod service { } DataKeyboard::KeyUp(enigo::Key::Raw(code)) => { if *code < 8 { - log::error!("Invalid Raw keycode {} (must be >= 8 due to XKB offset), skipping", code); + log::error!( + "Invalid Raw keycode {} (must be >= 8 due to XKB offset), skipping", + code + ); } else { let up_event = InputEvent::new(EventType::KEY, *code - 8, 0); allow_err!(keyboard.emit(&[up_event])); @@ -909,6 +919,35 @@ pub mod service { }); } + #[cfg(target_os = "linux")] + fn authorize_uinput_peer(postfix: &str, stream: &RawIpcConnection) -> bool { + if !hbb_common::config::is_service_ipc_postfix(postfix) { + return true; + } + let peer_uid = ipc::peer_uid_from_fd(stream.as_raw_fd()); + let active_uid = crate::platform::linux::get_active_userid_fresh() + .trim() + .parse::() + .ok(); + let authorized = + peer_uid.is_some_and(|uid| ipc::is_allowed_service_peer_uid(uid, active_uid)); + if !authorized { + crate::ipc::log_rejected_uinput_connection(postfix, peer_uid, active_uid); + return false; + } + if let Err(err) = + ipc::ensure_peer_executable_matches_current_by_fd(stream.as_raw_fd(), postfix) + { + log::warn!( + "Rejected connection on protected uinput ipc channel due to executable mismatch: postfix={}, err={}", + postfix, + err + ); + return false; + } + true + } + /// Start uinput service. async fn start_service(postfix: &str, handler: F) { match new_listener(postfix).await { @@ -916,6 +955,10 @@ pub mod service { while let Some(result) = incoming.next().await { match result { Ok(stream) => { + #[cfg(target_os = "linux")] + if !authorize_uinput_peer(postfix, &stream) { + continue; + } log::debug!("Got new connection of uinput ipc {}", postfix); handler(Connection::new(stream)); } From b757e97c11bf5e6b653acbd2fe74515f239b5947 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 10 May 2026 10:02:42 +0800 Subject: [PATCH 258/277] fix(translation): ja (#14993) Signed-off-by: fufesou --- src/lang/ja.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 20caca0a7..b55a6664f 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -739,7 +739,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Changelog", "更新履歴"), ("keep-awake-during-outgoing-sessions-label", "送信セッション中は、画面のスリープを無効化する"), ("keep-awake-during-incoming-sessions-label", "受信セッション中は、画面のスリープを無効化する"), - ("Continue with {}", "{}で続行する"), + ("Continue with {}", "{} で続行する"), ("Display Name", "表示名"), ("password-hidden-tip", "永続的なパスワードが設定されています (非表示)"), ("preset-password-in-use-tip", "プリセットパスワードが現在使用されています"), From 9c831dc59bd08d387db99283d72c7947602d3e23 Mon Sep 17 00:00:00 2001 From: fufesou Date: Sun, 10 May 2026 10:08:29 +0800 Subject: [PATCH 259/277] fix(fs): file transfer, reconnect, restore dir (#14925) * fix(fs): file transfer, reconnect, restore dir Signed-off-by: fufesou * fix(fs): simple refactor Signed-off-by: fufesou * fix(fs): simple refactor Signed-off-by: fufesou --------- Signed-off-by: fufesou --- flutter/lib/models/file_model.dart | 59 +++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 35001cbf2..7d91b03b3 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -391,14 +391,30 @@ class FileController { await Future.delayed(Duration(milliseconds: 100)); - final dir = (await bind.sessionGetPeerOption( + final savedDir = (await bind.sessionGetPeerOption( sessionId: sessionId, name: isLocal ? "local_dir" : "remote_dir")); - openDirectory(dir.isEmpty ? options.value.home : dir); + Future tryOpenReadyDirs() async { + final dirs = { + if (directory.value.path.isNotEmpty) directory.value.path, + if (savedDir.isNotEmpty) savedDir, + options.value.home, + }; + for (final dir in dirs) { + if (await _openDirectoryPath(dir, isBack: true)) { + return true; + } + } + return false; + } + + var opened = await tryOpenReadyDirs(); await Future.delayed(Duration(seconds: 1)); - if (directory.value.path.isEmpty) { - openDirectory(options.value.home); + if (!opened) { + // The peer may become ready during the reconnect delay, so retry the + // same candidates instead of only retrying the default home directory. + await tryOpenReadyDirs(); } } @@ -429,19 +445,23 @@ class FileController { }); } - Future refresh() async { - await openDirectory(directory.value.path); + Future refresh() async { + // "." can be both a refresh command and a real remote directory path. + // Refresh must bypass openDirectory's command dispatch to avoid recursion. + return await _openDirectoryPath(directory.value.path, isBack: true); } - Future openDirectory(String path, {bool isBack = false}) async { - if (path == ".") { - refresh(); - return; + Future openDirectory(String path, {bool isBack = false}) async { + if (!isBack && path == ".") { + return await refresh(); } - if (path == "..") { - goToParentDirectory(); - return; + if (!isBack && path == "..") { + return await _goToParentDirectory(isBack: isBack); } + return await _openDirectoryPath(path, isBack: isBack); + } + + Future _openDirectoryPath(String path, {bool isBack = false}) async { if (!isBack) { pushHistory(); } @@ -458,8 +478,10 @@ class FileController { final fd = await fileFetcher.fetchDirectory(path, isLocal, showHidden); fd.format(isWindows, sort: sortBy.value); directory.value = fd; + return true; } catch (e) { debugPrint("Failed to openDirectory $path: $e"); + return false; } } @@ -487,19 +509,22 @@ class FileController { goBack(); return; } - openDirectory(path, isBack: true); + unawaited(_openDirectoryPath(path, isBack: true).then((_) {})); } void goToParentDirectory() { + unawaited(_goToParentDirectory().then((_) {})); + } + + Future _goToParentDirectory({bool isBack = false}) async { final isWindows = options.value.isWindows; final dirPath = directory.value.path; var parent = PathUtil.dirname(dirPath, isWindows); // specially for C:\, D:\, goto '/' if (parent == dirPath && isWindows) { - openDirectory('/'); - return; + return await _openDirectoryPath('/', isBack: isBack); } - openDirectory(parent); + return await _openDirectoryPath(parent, isBack: isBack); } // TODO deprecated this From 0e4b91b8d7c352f56d7aca829e944eba747ac804 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 11 May 2026 12:58:01 +0800 Subject: [PATCH 260/277] =?UTF-8?q?Harden=20os=20password=20=EF=BC=88termi?= =?UTF-8?q?nal=20windows=20and=20headless=20linux)=20anti=20brute=20force?= =?UTF-8?q?=20(#14985)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(windows): terminal, preauth bruteforce Signed-off-by: fufesou * fix(linux): headless, preauth bruteforce Signed-off-by: fufesou * fix(linux): headless, OS login, minimal fix Signed-off-by: fufesou * Terminal session, click-only Signed-off-by: fufesou * Simple refactor, logs Signed-off-by: fufesou * harden os password, better scoped failure set Signed-off-by: fufesou * harden os password, ip failure count Signed-off-by: fufesou * Check prelogin before starting cm Signed-off-by: fufesou * Isolate terminal OS login failure tracking Terminal OS login no longer reads or updates the default RustDesk per-IP failure bucket. It now uses only the OS credential policy, while RustDesk password attempts keep using the existing LOGIN_FAILURES[0] bucket. Signed-off-by: fufesou --------- Signed-off-by: fufesou --- src/platform/linux_desktop_manager.rs | 86 +++++- src/server.rs | 1 + src/server/connection.rs | 384 ++++++++++++++++++++++---- src/server/login_failure_check.rs | 231 ++++++++++++++++ 4 files changed, 633 insertions(+), 69 deletions(-) create mode 100644 src/server/login_failure_check.rs diff --git a/src/platform/linux_desktop_manager.rs b/src/platform/linux_desktop_manager.rs index 03f1f6250..0a512939b 100644 --- a/src/platform/linux_desktop_manager.rs +++ b/src/platform/linux_desktop_manager.rs @@ -2,7 +2,7 @@ use super::{linux::*, ResultType}; use crate::client::{ LOGIN_MSG_DESKTOP_NO_DESKTOP, LOGIN_MSG_DESKTOP_SESSION_ANOTHER_USER, LOGIN_MSG_DESKTOP_SESSION_NOT_READY, LOGIN_MSG_DESKTOP_XORG_NOT_FOUND, - LOGIN_MSG_DESKTOP_XSESSION_FAILED, + LOGIN_MSG_DESKTOP_XSESSION_FAILED, LOGIN_MSG_PASSWORD_WRONG, }; use hbb_common::{ allow_err, bail, log, @@ -94,6 +94,49 @@ fn detect_headless() -> Option<&'static str> { None } +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +enum XSessionStartErrorKind { + Auth, + Env, +} + +const XSESSION_AUTH_FAILURE_DETAIL: &str = "authentication failed"; + +#[derive(Debug)] +struct XSessionStartError { + kind: XSessionStartErrorKind, + detail: String, +} + +impl XSessionStartError { + fn auth(detail: String) -> Self { + Self { + kind: XSessionStartErrorKind::Auth, + detail, + } + } + + fn env(detail: String) -> Self { + Self { + kind: XSessionStartErrorKind::Env, + detail, + } + } +} + +impl std::fmt::Display for XSessionStartError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.detail) + } +} + +fn map_xsession_start_error_to_login_msg(kind: XSessionStartErrorKind) -> &'static str { + match kind { + XSessionStartErrorKind::Auth => LOGIN_MSG_PASSWORD_WRONG, + XSessionStartErrorKind::Env => LOGIN_MSG_DESKTOP_XSESSION_FAILED, + } +} + pub fn try_start_desktop(_username: &str, _passsword: &str) -> String { debug_assert!(crate::is_server()); if _username.is_empty() { @@ -136,14 +179,21 @@ pub fn try_start_desktop(_username: &str, _passsword: &str) -> String { } } Err(e) => { - log::error!("Failed to start xsession {}", e); - LOGIN_MSG_DESKTOP_XSESSION_FAILED.to_owned() + match e.kind { + XSessionStartErrorKind::Auth => { + log::warn!("Failed to authenticate xsession user {}", e); + } + XSessionStartErrorKind::Env => { + log::error!("Failed to start xsession {}", e); + } + } + map_xsession_start_error_to_login_msg(e.kind).to_owned() } } } } -fn try_start_x_session(username: &str, password: &str) -> ResultType<(String, bool)> { +fn try_start_x_session(username: &str, password: &str) -> Result<(String, bool), XSessionStartError> { let mut desktop_manager = DESKTOP_MANAGER.lock().unwrap(); if let Some(desktop_manager) = &mut (*desktop_manager) { if let Some(seat0_username) = desktop_manager.get_supported_display_seat0_username() { @@ -161,7 +211,9 @@ fn try_start_x_session(username: &str, password: &str) -> ResultType<(String, bo desktop_manager.is_running(), )) } else { - bail!(crate::client::LOGIN_MSG_DESKTOP_NOT_INITED); + Err(XSessionStartError::env( + crate::client::LOGIN_MSG_DESKTOP_NOT_INITED.to_owned(), + )) } } @@ -247,10 +299,15 @@ impl DesktopManager { self.is_child_running.load(Ordering::SeqCst) } - fn try_start_x_session(&mut self, username: &str, password: &str) -> ResultType<()> { + fn try_start_x_session( + &mut self, + username: &str, + password: &str, + ) -> Result<(), XSessionStartError> { match get_user_by_name(username) { Some(userinfo) => { - let mut client = pam::Client::with_password(&pam_get_service_name())?; + let mut client = pam::Client::with_password(&pam_get_service_name()) + .map_err(|e| XSessionStartError::env(format!("failed to init pam client, {}", e)))?; client .conversation_mut() .set_credentials(username, password); @@ -267,17 +324,24 @@ impl DesktopManager { Ok(()) } Err(e) => { - bail!("failed to start x session, {}", e); + Err(XSessionStartError::env(format!( + "failed to start x session, {}", + e + ))) } } } - Err(e) => { - bail!("failed to check user pass for {}, {}", username, e); + Err(_e) => { + Err(XSessionStartError::auth( + XSESSION_AUTH_FAILURE_DETAIL.to_owned(), + )) } } } None => { - bail!("failed to get userinfo of {}", username); + Err(XSessionStartError::auth( + XSESSION_AUTH_FAILURE_DETAIL.to_owned(), + )) } } } diff --git a/src/server.rs b/src/server.rs index e11003faa..86f7b5396 100644 --- a/src/server.rs +++ b/src/server.rs @@ -67,6 +67,7 @@ pub mod input_service { } mod connection; +mod login_failure_check; pub mod display_service; #[cfg(windows)] pub mod portable_service; diff --git a/src/server/connection.rs b/src/server/connection.rs index f5019e447..538503d9c 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1,3 +1,8 @@ +#[cfg(target_os = "windows")] +use super::login_failure_check::try_acquire_os_credential_login_gate; +use super::login_failure_check::{ + evaluate_os_credential_policy, record_os_credential_failure, FailureScope, +}; use super::{input_service::*, *}; #[cfg(feature = "unix-file-copy-paste")] use crate::clipboard::try_empty_clipboard_files; @@ -82,6 +87,9 @@ lazy_static::lazy_static! { static ref PENDING_SWITCH_SIDES_UUID: Arc::>> = Default::default(); } +#[cfg(target_os = "windows")] +const TERMINAL_OS_LOGIN_FAILED_MSG: &str = "Incorrect username or password."; + fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { if a.len() != b.len() { return false; @@ -94,6 +102,32 @@ fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { x == 0 } +#[cfg(target_os = "linux")] +fn should_check_linux_headless_os_auth_before_desktop_start( + is_headless_allowed: bool, + username: &str, +) -> bool { + is_headless_allowed + && !username.trim().is_empty() + && linux_desktop_manager::get_username().is_empty() +} + +#[cfg(target_os = "linux")] +fn should_record_linux_headless_os_auth_failure( + is_headless_allowed: bool, + username: &str, + err_msg: &str, +) -> bool { + is_headless_allowed + && !username.trim().is_empty() + && err_msg == crate::client::LOGIN_MSG_PASSWORD_WRONG +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +fn should_use_terminal_os_login_scope(is_terminal: bool, os_login_username: &str) -> bool { + cfg!(target_os = "windows") && is_terminal && !os_login_username.trim().is_empty() +} + #[cfg(any(target_os = "windows", target_os = "linux"))] lazy_static::lazy_static! { static ref WALLPAPER_REMOVER: Arc>> = Default::default(); @@ -1497,6 +1531,9 @@ impl Connection { // Keep the connection alive so the client can continue with 2FA. return true; } + if let Some(keep_alive) = self.prepare_terminal_login_for_authorization().await { + return keep_alive; + } if !self.connect_port_forward_if_needed().await { return false; } @@ -2376,33 +2413,6 @@ impl Connection { o.terminal_persistent.enum_value() == Ok(BoolOption::Yes); } self.terminal_service_id = terminal.service_id; - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if let Some(msg) = - self.fill_terminal_user_token(&lr.os_login.username, &lr.os_login.password) - { - self.send_login_error(msg).await; - sleep(1.).await; - return false; - } - - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if let Some(is_user) = - terminal_service::is_service_specified_user(&self.terminal_service_id) - { - if let Some(user_token) = &self.terminal_user_token { - let has_service_token = - user_token.to_terminal_service_token().is_some(); - if is_user != has_service_token { - // This occurs when the service id (in the configuration) is manually changed by the user, causing a mismatch in validation. - log::error!("Terminal service user mismatch detected. The service ID may have been manually changed in the configuration, causing validation to fail."); - // No need to translate the following message, because it is in an abnormal case. - self.send_login_error("Terminal service user mismatch detected.") - .await; - sleep(1.).await; - return false; - } - } - } } Some(login_request::Union::PortForward(mut pf)) => { if !Self::permission(keys::OPTION_ENABLE_TUNNEL, &self.control_permissions) { @@ -2420,8 +2430,43 @@ impl Connection { } } + if !hbb_common::is_ip_str(&lr.username) + && !hbb_common::is_domain_port_str(&lr.username) + && lr.username != Config::get_id() + { + self.send_login_error(crate::client::LOGIN_MSG_OFFLINE) + .await; + return false; + } + + #[cfg(target_os = "windows")] + if self.terminal + && lr.os_login.username.trim().is_empty() + && crate::platform::is_prelogin() + { + self.send_login_error( + "No active console user logged on, please connect and logon first.", + ) + .await; + sleep(1.).await; + return false; + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] - self.try_start_cm_ipc(); + if !should_use_terminal_os_login_scope(self.terminal, &lr.os_login.username) { + self.try_start_cm_ipc(); + } + + #[cfg(target_os = "linux")] + if should_check_linux_headless_os_auth_before_desktop_start( + self.linux_headless_handle.is_headless_allowed, + &lr.os_login.username, + ) { + let (_failure, res) = self.check_failure(0).await; + if !res { + return true; + } + } #[cfg(not(target_os = "linux"))] let err_msg = "".to_owned(); @@ -2433,6 +2478,18 @@ impl Connection { // If err is LOGIN_MSG_DESKTOP_SESSION_NOT_READY, just keep this msg and go on checking password. if !err_msg.is_empty() && err_msg != crate::client::LOGIN_MSG_DESKTOP_SESSION_NOT_READY { + #[cfg(target_os = "linux")] + if should_record_linux_headless_os_auth_failure( + self.linux_headless_handle.is_headless_allowed, + &lr.os_login.username, + &err_msg, + ) { + let (failure, res) = self.check_failure(0).await; + if !res { + return true; + } + self.update_failure(failure, false, 0); + } self.send_login_error(err_msg).await; return true; } @@ -2461,17 +2518,16 @@ impl Connection { crate::get_builtin_option(keys::OPTION_ALLOW_LOGON_SCREEN_PASSWORD) == "Y" && is_logon(); - if !hbb_common::is_ip_str(&lr.username) - && !hbb_common::is_domain_port_str(&lr.username) - && lr.username != Config::get_id() - { - self.send_login_error(crate::client::LOGIN_MSG_OFFLINE) - .await; - return false; - } else if (password::approve_mode() == ApproveMode::Click - && !allow_logon_screen_password) + if (password::approve_mode() == ApproveMode::Click && !allow_logon_screen_password) || password::approve_mode() == ApproveMode::Both && !password::has_valid_password() { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if should_use_terminal_os_login_scope(self.terminal, &lr.os_login.username) { + if let Some(keep_alive) = self.prepare_terminal_login_for_authorization().await + { + return keep_alive; + } + } self.try_start_cm(lr.my_id, lr.my_name, false); if hbb_common::get_version_number(&lr.version) >= hbb_common::get_version_number("1.2.0") @@ -2493,6 +2549,14 @@ impl Connection { } } else if lr.password.is_empty() { if err_msg.is_empty() { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if should_use_terminal_os_login_scope(self.terminal, &lr.os_login.username) { + if let Some(keep_alive) = + self.prepare_terminal_login_for_authorization().await + { + return keep_alive; + } + } self.try_start_cm(lr.my_id, lr.my_name, false); } else { self.send_login_error( @@ -2506,7 +2570,7 @@ impl Connection { return true; } if !self.validate_password(allow_logon_screen_password) { - self.update_failure(failure, false, 0); + self.update_failure_with_scope(failure, false, 0, FailureScope::Default); self.check_update_temporary_password(false); if err_msg.is_empty() { self.send_login_error(crate::client::LOGIN_MSG_PASSWORD_WRONG) @@ -2519,7 +2583,7 @@ impl Connection { .await; } } else { - self.update_failure(failure, true, 0); + self.update_failure_with_scope(failure, true, 0, FailureScope::Default); if err_msg.is_empty() { #[cfg(target_os = "linux")] self.linux_headless_handle.wait_desktop_cm_ready().await; @@ -3484,16 +3548,16 @@ impl Connection { self.terminal_user_token = Some(TerminalUserToken::SelfUser); None } else { - Some("The user is not an administrator.") + Some(TERMINAL_OS_LOGIN_FAILED_MSG) } } Ok(Err(e)) => { log::error!("Failed to check if the user is an administrator: {}", e); - Some("Failed to check if the user is an administrator.") + Some(TERMINAL_OS_LOGIN_FAILED_MSG) } Err(e) => { log::error!("Failed to get logon user token: {}", e); - Some("Incorrect username or password.") + Some(TERMINAL_OS_LOGIN_FAILED_MSG) } } } @@ -3529,6 +3593,146 @@ impl Connection { } } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + async fn prepare_terminal_login_for_authorization(&mut self) -> Option { + if !self.terminal || self.terminal_user_token.is_some() { + return None; + } + + #[derive(Copy, Clone)] + enum TerminalAuthorizationMode { + OsLogin { + failure: ((i32, i32, i32), i32), + scope: FailureScope, + }, + SessionUser, + } + + let normalized_username = self.lr.os_login.username.trim().to_owned(); + let auth_mode = if should_use_terminal_os_login_scope(self.terminal, &normalized_username) { + // Check failure state + let failure_scope = FailureScope::TerminalOsLogin; + let (failure, res) = self.check_failure_with_scope(0, failure_scope).await; + if !res { + log::warn!( + "OS credential login blocked by failure policy: ip={} conn_id={} scope={:?}", + self.ip, + self.inner.id(), + failure_scope + ); + // Terminal OS login is sensitive. Close this connection instead of keeping it + // alive for retries on the same socket after a rate-limit block. + return Some(false); + } + TerminalAuthorizationMode::OsLogin { + failure, + scope: failure_scope, + } + } else { + TerminalAuthorizationMode::SessionUser + }; + + let is_terminal_os_login = matches!(auth_mode, TerminalAuthorizationMode::OsLogin { .. }); + let failure_scope = match auth_mode { + TerminalAuthorizationMode::OsLogin { scope, .. } => scope, + TerminalAuthorizationMode::SessionUser => FailureScope::Default, + }; + + let username = normalized_username; + let password = self.lr.os_login.password.clone(); + let terminal_login_error = { + #[cfg(target_os = "windows")] + { + // Concurrency gate for terminal OS login with credentials, to prevent brute-force attacks. + let _os_login_concurrency_guard = if is_terminal_os_login { + let guard = try_acquire_os_credential_login_gate(); + if guard.is_err() { + log::warn!( + "OS credential login blocked by concurrency gate: ip={} conn_id={} scope={:?}", + self.ip, + self.inner.id(), + failure_scope + ); + self.send_login_error("Please try 1 minute later").await; + sleep(1.).await; + Self::post_alarm_audit( + AlarmAuditType::TerminalOsLoginConcurrency, + json!({ + "ip": self.ip, + "id": self.lr.my_id.clone(), + "name": self.lr.my_name.clone(), + }), + ); + return Some(false); + } + guard.ok() + } else { + None + }; + self.fill_terminal_user_token(&username, &password) + } + #[cfg(not(target_os = "windows"))] + { + self.fill_terminal_user_token(&username, &password) + } + }; + if let Some(msg) = terminal_login_error { + if let TerminalAuthorizationMode::OsLogin { failure, scope } = auth_mode { + self.update_failure_with_scope(failure, false, 0, scope); + } + let auth_context = if is_terminal_os_login { + "OS credential login verification" + } else { + "Terminal session-user authorization" + }; + log::warn!( + "{} failed: ip={} conn_id={} scope={:?} msg='{}'", + auth_context, + self.ip, + self.inner.id(), + failure_scope, + msg + ); + self.send_login_error(msg).await; + sleep(1.).await; + return Some(false); + } + if let TerminalAuthorizationMode::OsLogin { failure, scope } = auth_mode { + self.update_failure_with_scope(failure, true, 0, scope); + } + + if let Some(is_user) = + terminal_service::is_service_specified_user(&self.terminal_service_id) + { + if let Some(user_token) = &self.terminal_user_token { + let has_service_token = user_token.to_terminal_service_token().is_some(); + if is_user != has_service_token { + log::error!( + "Terminal service user mismatch: ip={} conn_id={} service_is_user={} has_service_token={}. The service ID may have been manually changed in the configuration, causing validation to fail.", + self.ip, + self.inner.id(), + is_user, + has_service_token + ); + // No need to translate the following message, because it is in an abnormal case. + self.send_login_error("Terminal service user mismatch detected.") + .await; + sleep(1.).await; + return Some(false); + } + } + } + if is_terminal_os_login { + self.try_start_cm_ipc(); + } + None + } + + #[cfg(any(target_os = "android", target_os = "ios"))] + async fn prepare_terminal_login_for_authorization(&mut self) -> Option { + None + } + // Try to parse connection IP as IPv6 address, returning /64, /56, and /48 prefixes. // Parsing an IPv4 address just returns None. // note: we specifically don't use hbb_common::is_ipv6_str to avoid divergence issues @@ -3555,18 +3759,37 @@ impl Connection { Some((p64, p56, p48)) } - fn update_failure(&self, (failure, time): ((i32, i32, i32), i32), remove: bool, i: usize) { - fn bump(mut cur: (i32, i32, i32), time: i32) -> (i32, i32, i32) { - if cur.0 == time { - cur.1 += 1; - cur.2 += 1; - } else { - cur.0 = time; - cur.1 = 1; - cur.2 += 1; - } - cur + fn bump_failure_entry(mut cur: (i32, i32, i32), time: i32) -> (i32, i32, i32) { + if cur.0 == time { + cur.1 += 1; + cur.2 += 1; + } else { + cur.0 = time; + cur.1 = 1; + cur.2 += 1; } + cur + } + + fn update_failure(&self, failure: ((i32, i32, i32), i32), remove: bool, i: usize) { + self.update_failure_with_scope(failure, remove, i, FailureScope::Default); + } + + fn update_failure_with_scope( + &self, + (failure, time): ((i32, i32, i32), i32), + remove: bool, + i: usize, + scope: FailureScope, + ) { + let os_credential_scope = matches!(scope, FailureScope::TerminalOsLogin); + if os_credential_scope { + if !remove { + record_os_credential_failure(scope); + } + return; + } + let map_mutex = &LOGIN_FAILURES[i]; if remove { if failure.0 != 0 { @@ -3587,14 +3810,15 @@ impl Connection { let mut m = map_mutex.lock().unwrap(); for key in [p64, p56, p48] { let cur = m.get(&key).copied().unwrap_or((0, 0, 0)); - m.insert(key, bump(cur, time)); + m.insert(key, Self::bump_failure_entry(cur, time)); } - // Update full IP: bump from the *original* passed-in failure - m.insert(self.ip.clone(), bump(failure, time)); + let current_ip = m.get(&self.ip).copied().unwrap_or((0, 0, 0)); + m.insert(self.ip.clone(), Self::bump_failure_entry(current_ip, time)); } else { - // Update full IP: bump from the *original* passed-in failure + // Re-read the full IP bucket in case another failed attempt updated it. let mut m = map_mutex.lock().unwrap(); - m.insert(self.ip.clone(), bump(failure, time)); + let current_ip = m.get(&self.ip).copied().unwrap_or((0, 0, 0)); + m.insert(self.ip.clone(), Self::bump_failure_entry(current_ip, time)); } } @@ -3634,8 +3858,50 @@ impl Connection { } async fn check_failure(&mut self, i: usize) -> (((i32, i32, i32), i32), bool) { + self.check_failure_with_scope(i, FailureScope::Default) + .await + } + + async fn check_failure_with_scope( + &mut self, + i: usize, + scope: FailureScope, + ) -> (((i32, i32, i32), i32), bool) { let time = (get_time() / 60_000) as i32; + if matches!(scope, FailureScope::TerminalOsLogin) { + let decision = evaluate_os_credential_policy(scope, get_time()); + let res = if decision.allowed { + true + } else { + log::warn!( + "OS credential login blocked by policy: ip={} conn_id={} i={} msg='{}'", + self.ip, + self.inner.id(), + i, + decision.login_error.as_deref().unwrap_or("") + ); + if let Some(login_error) = decision.login_error { + // Rare branch and currently temporary response copy; translation can be added later if needed. + self.send_login_error(login_error).await; + } + if let Some(audit) = decision.audit { + // For OS blocked/backoff events, we currently emit one alarm report per blocked attempt. + // TODO: Add unified cumulative/aggregation fields across alarm producers. + Self::post_alarm_audit( + audit, + json!({ + "ip": self.ip, + "id": self.lr.my_id.clone(), + "name": self.lr.my_name.clone(), + }), + ); + } + false + }; + return (((0, 0, 0), time), res); + } + // IPv6 addresses are cheap to make so we check prefix/netblock as well if let Some((p64, p56, p48)) = self.get_ipv6_prefixes() { if let Some(res) = self.check_failure_ipv6_prefix(i, time, &p64, 64, 60).await { @@ -5219,6 +5485,8 @@ pub enum AlarmAuditType { // MultipleLoginsAttemptsWithinOneMinute = 4, // MultipleLoginsAttemptsWithinOneHour = 5, ExceedIPv6PrefixAttempts = 6, + TerminalOsLoginBackoff = 7, + TerminalOsLoginConcurrency = 8, } pub enum FileAuditType { diff --git a/src/server/login_failure_check.rs b/src/server/login_failure_check.rs new file mode 100644 index 000000000..4394213ec --- /dev/null +++ b/src/server/login_failure_check.rs @@ -0,0 +1,231 @@ +use crate::AlarmAuditType; +use hbb_common::get_time; +#[cfg(target_os = "windows")] +use hbb_common::tokio::sync::{Mutex as TokioMutex, OwnedMutexGuard}; +use std::sync::Mutex; +#[cfg(target_os = "windows")] +use std::sync::Arc; + +const OS_CREDENTIAL_LOGIN_TOTAL_IDLE_RESET_MS: i64 = 120 * 60 * 1_000; +const OS_CREDENTIAL_LOGIN_BACKOFF_BASE_SECONDS: i64 = 15; +const OS_CREDENTIAL_LOGIN_BACKOFF_MAX_SECONDS: i64 = 30 * 60; + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub(crate) enum FailureScope { + Default, + TerminalOsLogin, +} + +pub(crate) struct OsCredentialPolicyDecision { + pub allowed: bool, + pub login_error: Option, + pub audit: Option, +} + +#[derive(Copy, Clone, Debug, Default)] +struct OsCredentialFailureState { + total_failures: i32, + backoff_until_ms: Option, + last_failure_ms: Option, +} + +lazy_static::lazy_static! { + static ref OS_CREDENTIAL_LOGIN_FAILURE_STATE: Mutex = + Mutex::new(OsCredentialFailureState::default()); +} + +#[cfg(target_os = "windows")] +lazy_static::lazy_static! { + static ref OS_CREDENTIAL_LOGIN_MUTEX: Arc> = Arc::new(TokioMutex::new(())); +} + +fn is_os_credential_scope(scope: FailureScope) -> bool { + matches!(scope, FailureScope::TerminalOsLogin) +} + +fn state_for_os_credential_scope( + scope: FailureScope, +) -> Option<&'static Mutex> { + if is_os_credential_scope(scope) { + Some(&OS_CREDENTIAL_LOGIN_FAILURE_STATE) + } else { + None + } +} + +fn backoff_audit_type_for_scope(scope: FailureScope) -> Option { + match scope { + FailureScope::TerminalOsLogin => Some(AlarmAuditType::TerminalOsLoginBackoff), + FailureScope::Default => None, + } +} + +fn os_credential_login_backoff_seconds(total_failures: i32) -> i64 { + if total_failures <= 2 { + return 0; + } + let exp = (total_failures - 3).min(7); + let seconds = OS_CREDENTIAL_LOGIN_BACKOFF_BASE_SECONDS * (1_i64 << exp); + seconds.min(OS_CREDENTIAL_LOGIN_BACKOFF_MAX_SECONDS) +} + +fn normalize_backoff(state: &mut OsCredentialFailureState, now_ms: i64) { + if let Some(until_ms) = state.backoff_until_ms { + if until_ms <= now_ms { + state.backoff_until_ms = None; + } + } +} + +fn reset_totals_on_idle(state: &mut OsCredentialFailureState, now_ms: i64) { + if let Some(last_ms) = state.last_failure_ms { + if now_ms.saturating_sub(last_ms) >= OS_CREDENTIAL_LOGIN_TOTAL_IDLE_RESET_MS { + state.total_failures = 0; + state.backoff_until_ms = None; + state.last_failure_ms = None; + } + } +} + +fn allow_decision() -> OsCredentialPolicyDecision { + OsCredentialPolicyDecision { + allowed: true, + login_error: None, + audit: None, + } +} + +fn block_decision( + login_error: String, + alarm_type: Option, +) -> OsCredentialPolicyDecision { + OsCredentialPolicyDecision { + allowed: false, + login_error: Some(login_error), + audit: alarm_type, + } +} + +pub(crate) fn evaluate_os_credential_policy( + scope: FailureScope, + now_ms: i64, +) -> OsCredentialPolicyDecision { + if !is_os_credential_scope(scope) { + return allow_decision(); + } + let Some(state_mutex) = state_for_os_credential_scope(scope) else { + return allow_decision(); + }; + let mut state = state_mutex.lock().unwrap(); + reset_totals_on_idle(&mut state, now_ms); + normalize_backoff(&mut state, now_ms); + + if let Some(until_ms) = state.backoff_until_ms { + let remaining_ms = (until_ms - now_ms).max(0); + let remaining_seconds = ((remaining_ms + 999) / 1_000).max(1); + let seconds_label = if remaining_seconds == 1 { + "second" + } else { + "seconds" + }; + block_decision( + format!( + "Please try again in {} {}.", + remaining_seconds, seconds_label + ), + backoff_audit_type_for_scope(scope), + ) + } else { + allow_decision() + } +} + +pub(crate) fn record_os_credential_failure(scope: FailureScope) { + if !is_os_credential_scope(scope) { + return; + } + let Some(state_mutex) = state_for_os_credential_scope(scope) else { + return; + }; + let mut state = state_mutex.lock().unwrap(); + let now_ms = get_time(); + reset_totals_on_idle(&mut state, now_ms); + normalize_backoff(&mut state, now_ms); + state.total_failures = state.total_failures.saturating_add(1); + state.last_failure_ms = Some(now_ms); + let backoff_seconds = os_credential_login_backoff_seconds(state.total_failures); + if backoff_seconds > 0 { + state.backoff_until_ms = Some(now_ms + backoff_seconds * 1_000); + } +} + +#[cfg(target_os = "windows")] +pub(crate) fn try_acquire_os_credential_login_gate() -> Result, ()> { + OS_CREDENTIAL_LOGIN_MUTEX + .clone() + .try_lock_owned() + .map_err(|_| ()) +} + +#[cfg(test)] +mod tests { + use super::*; + + static TEST_MUTEX: Mutex<()> = Mutex::new(()); + + fn clear_os_credential_failure_state(scope: FailureScope) { + if let Some(state_mutex) = state_for_os_credential_scope(scope) { + *state_mutex.lock().unwrap() = OsCredentialFailureState::default(); + } + } + + #[test] + fn os_credential_policy_prioritizes_backoff() { + let _guard = TEST_MUTEX.lock().unwrap(); + clear_os_credential_failure_state(FailureScope::TerminalOsLogin); + let now_ms = get_time(); + for _ in 0..3 { + record_os_credential_failure(FailureScope::TerminalOsLogin); + } + let decision = evaluate_os_credential_policy(FailureScope::TerminalOsLogin, now_ms); + assert!(!decision.allowed); + assert!(decision.login_error.is_some()); + clear_os_credential_failure_state(FailureScope::TerminalOsLogin); + } + + #[test] + fn os_credential_policy_idle_window_resets_total_counter() { + let _guard = TEST_MUTEX.lock().unwrap(); + clear_os_credential_failure_state(FailureScope::TerminalOsLogin); + for _ in 0..13 { + record_os_credential_failure(FailureScope::TerminalOsLogin); + } + let blocked = evaluate_os_credential_policy(FailureScope::TerminalOsLogin, get_time()); + assert!(!blocked.allowed); + + let after_failures_ms = get_time(); + let after_idle_ms = after_failures_ms + OS_CREDENTIAL_LOGIN_TOTAL_IDLE_RESET_MS + 1_000; + let allowed = evaluate_os_credential_policy(FailureScope::TerminalOsLogin, after_idle_ms); + assert!(allowed.allowed); + clear_os_credential_failure_state(FailureScope::TerminalOsLogin); + } + + #[test] + fn os_credential_policy_audits_every_backoff_block() { + let _guard = TEST_MUTEX.lock().unwrap(); + clear_os_credential_failure_state(FailureScope::TerminalOsLogin); + + for _ in 0..3 { + record_os_credential_failure(FailureScope::TerminalOsLogin); + } + let now_ms = get_time(); + let first = evaluate_os_credential_policy(FailureScope::TerminalOsLogin, now_ms); + let second = evaluate_os_credential_policy(FailureScope::TerminalOsLogin, now_ms + 1_000); + assert!(!first.allowed); + assert!(!second.allowed); + assert!(first.audit.is_some()); + assert!(second.audit.is_some()); + + clear_os_credential_failure_state(FailureScope::TerminalOsLogin); + } +} From 1978020d275f22ac2478232ccda691b63c7d90d0 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 11 May 2026 12:58:32 +0800 Subject: [PATCH 261/277] fix(custom-client): desktop, incoming only, touch drag (#14928) Signed-off-by: fufesou --- flutter/lib/desktop/widgets/tabbar_widget.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index ef195b493..9ef7d38d9 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -593,13 +593,13 @@ class _DesktopTabState extends State } Widget _buildBar() { + final isIncomingHomePage = bind.isIncomingOnly() && isInHomePage(); return Row( children: [ Expanded( child: GestureDetector( // custom double tap handler - onTap: !(bind.isIncomingOnly() && isInHomePage()) && - showMaximize + onTap: !isIncomingHomePage && showMaximize ? () { final current = DateTime.now().millisecondsSinceEpoch; final elapsed = current - _lastClickTime; @@ -610,7 +610,7 @@ class _DesktopTabState extends State .then((value) => stateGlobal.setMaximized(value)); } } - : null, + : (isIncomingHomePage ? () {} : null), // Keep tap recognizer for Windows touch. onPanStart: (_) => startDragging(isMainWindow), onPanCancel: () { // We want to disable dragging of the tab area in the tab bar. From d8808baa83347f4f8e3364fd4bd15e22891515e4 Mon Sep 17 00:00:00 2001 From: Yan Wang Date: Mon, 11 May 2026 12:58:49 +0800 Subject: [PATCH 262/277] Allow macOS monitor switching in privacy mode (#15004) Co-authored-by: Codex --- flutter/lib/common/widgets/toolbar.dart | 12 ++++++++++-- flutter/lib/desktop/widgets/remote_toolbar.dart | 3 ++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index 2e7247d95..537014246 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -16,6 +16,12 @@ import 'package:get/get.dart'; bool isEditOsPassword = false; +// macOS privacy mode blacks out all online displays, so switching the remote +// display does not weaken the local privacy protection. +bool allowDisplaySwitchInPrivacyMode(PeerInfo pi) { + return pi.platform == kPeerPlatformMacOS; +} + class TTextMenu { final Widget child; final VoidCallback? onPressed; @@ -684,8 +690,9 @@ Future> toolbarDisplayToggle( child: Text(translate('Lock after session end')))); } + final privacyModeState = PrivacyModeState.find(id); if (pi.isSupportMultiDisplay && - PrivacyModeState.find(id).isEmpty && + (privacyModeState.isEmpty || allowDisplaySwitchInPrivacyMode(pi)) && pi.displaysCount.value > 1 && bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') { final value = @@ -776,7 +783,8 @@ List toolbarPrivacyMode( onChanged: enabled ? (value) { if (value == null) return; - if (ffiModel.pi.currentDisplay != 0 && + if (!allowDisplaySwitchInPrivacyMode(pi) && + ffiModel.pi.currentDisplay != 0 && ffiModel.pi.currentDisplay != kAllDisplayValue) { msgBox( sessionId, diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 5da253e80..645cbe1cb 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -376,7 +376,8 @@ class _RemoteToolbarState extends State { } toolbarItems.add(Obx(() { - if (PrivacyModeState.find(widget.id).isEmpty && + if ((PrivacyModeState.find(widget.id).isEmpty || + allowDisplaySwitchInPrivacyMode(pi)) && pi.displaysCount.value > 1) { return _MonitorMenu( id: widget.id, From 55c9707639c40d78731cfff6ea7aaaddc6e8542a Mon Sep 17 00:00:00 2001 From: fufesou Date: Tue, 12 May 2026 16:24:50 +0800 Subject: [PATCH 263/277] fix(msi): check install folder, remove files when uninstall (#15011) * fix(msi): check install folder, remove files when uninstall Signed-off-by: fufesou * fix(msi): harden install folder normalization cleanup Signed-off-by: fufesou * fix(msi): better file attributes Signed-off-by: fufesou * fix(mis): Simple refactor Signed-off-by: fufesou * fix(msi): avoid path-based attribute changes in cleanup Signed-off-by: fufesou * fix(msi): custom action, unset flag read before del Signed-off-by: fufesou --------- Signed-off-by: fufesou --- res/msi/CustomActions/CustomActions.cpp | 169 +++++++++----------- res/msi/CustomActions/CustomActions.def | 2 +- res/msi/Package/Components/Folders.wxs | 11 +- res/msi/Package/Components/RustDesk.wxs | 16 +- res/msi/Package/Fragments/CustomActions.wxs | 2 +- res/msi/Package/UI/MyInstallDlg.wxs | 16 +- 6 files changed, 109 insertions(+), 107 deletions(-) diff --git a/res/msi/CustomActions/CustomActions.cpp b/res/msi/CustomActions/CustomActions.cpp index 0107929f3..f4780dd87 100644 --- a/res/msi/CustomActions/CustomActions.cpp +++ b/res/msi/CustomActions/CustomActions.cpp @@ -31,17 +31,17 @@ LExit: return WcaFinalize(er); } -// Helper function to safely delete a file or directory using handle-based deletion. -// This avoids TOCTOU (Time-Of-Check-Time-Of-Use) race conditions. +// Helper function to safely delete a file using handle-based deletion. +// Directories are refused after opening the handle. BOOL SafeDeleteItem(LPCWSTR fullPath) { - // Open the file/directory with DELETE access and FILE_FLAG_OPEN_REPARSE_POINT + // Open the file/directory with delete and attribute-read access plus FILE_FLAG_OPEN_REPARSE_POINT // to prevent following symlinks. // Use shared access to allow deletion even when other processes have the file open. DWORD flags = FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT; HANDLE hFile = CreateFileW( fullPath, - DELETE, + DELETE | FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, // Allow shared access NULL, OPEN_EXISTING, @@ -55,6 +55,21 @@ BOOL SafeDeleteItem(LPCWSTR fullPath) return FALSE; } + BY_HANDLE_FILE_INFORMATION fileInfo; + if (FALSE == GetFileInformationByHandle(hFile, &fileInfo)) + { + WcaLog(LOGMSG_STANDARD, "SafeDeleteItem: Failed to inspect '%ls'. Error: %lu", fullPath, GetLastError()); + CloseHandle(hFile); + return FALSE; + } + + if (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + { + WcaLog(LOGMSG_STANDARD, "SafeDeleteItem: Refusing to delete directory '%ls'.", fullPath); + CloseHandle(hFile); + return FALSE; + } + // Use SetFileInformationByHandle to mark for deletion. // The file will be deleted when the handle is closed. FILE_DISPOSITION_INFO dispInfo; @@ -77,98 +92,74 @@ BOOL SafeDeleteItem(LPCWSTR fullPath) return result; } -// Helper function to recursively delete a directory's contents with detailed logging. -void RecursiveDelete(LPCWSTR path) +BOOL PathEndsWithSlash(LPCWSTR path) { - // Ensure the path is not empty or null. - if (path == NULL || path[0] == L'\0') + size_t length = 0; + HRESULT hr = StringCchLengthW(path, MAX_PATH, &length); + if (FAILED(hr) || length == 0) + { + return FALSE; + } + + WCHAR last = path[length - 1]; + return last == L'\\' || last == L'/'; +} + +void ClearReadOnlyAttribute(LPCWSTR fullPath, DWORD attributes) +{ + if (!(attributes & FILE_ATTRIBUTE_READONLY)) { return; } - // Extra safety: never operate directly on a root path. - if (PathIsRootW(path)) + DWORD writableAttributes = attributes & ~FILE_ATTRIBUTE_READONLY; + if (writableAttributes == 0) { - WcaLog(LOGMSG_STANDARD, "RecursiveDelete: refusing to operate on root path '%ls'.", path); + writableAttributes = FILE_ATTRIBUTE_NORMAL; + } + + if (SetFileAttributesW(fullPath, writableAttributes)) + { + WcaLog(LOGMSG_STANDARD, "Runtime cleanup cleared read-only attribute for '%ls'.", fullPath); return; } - // MAX_PATH is enough here since the installer should not be using longer paths. - // No need to handle extended-length paths (\\?\) in this context. - WCHAR searchPath[MAX_PATH]; - HRESULT hr = StringCchPrintfW(searchPath, MAX_PATH, L"%s\\*", path); - if (FAILED(hr)) { - WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Path too long to enumerate: %ls", path); - return; + WcaLog(LOGMSG_STANDARD, "Runtime cleanup failed to clear read-only attribute for '%ls'. Error: %lu", fullPath, GetLastError()); +} + +BOOL DeleteRuntimeGeneratedFile(LPCWSTR installFolder, LPCWSTR fileName) +{ + WCHAR fullPath[MAX_PATH]; + LPCWSTR separator = PathEndsWithSlash(installFolder) ? L"" : L"\\"; + HRESULT hr = StringCchPrintfW(fullPath, MAX_PATH, L"%s%s%s", installFolder, separator, fileName); + if (FAILED(hr)) + { + WcaLog(LOGMSG_STANDARD, "Runtime cleanup path is too long for '%ls'.", fileName); + return FALSE; } - WIN32_FIND_DATAW findData; - HANDLE hFind = FindFirstFileW(searchPath, &findData); - - if (hFind == INVALID_HANDLE_VALUE) + DWORD attributes = GetFileAttributesW(fullPath); + if (attributes == INVALID_FILE_ATTRIBUTES) { - // This can happen if the directory is empty or doesn't exist, which is not an error in our case. - WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Failed to enumerate directory '%ls'. It may be missing or inaccessible. Error: %lu", path, GetLastError()); - return; + DWORD error = GetLastError(); + if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND) + { + return TRUE; + } + + WcaLog(LOGMSG_STANDARD, "Runtime cleanup cannot stat '%ls'. Error: %lu", fullPath, error); + return FALSE; } - do + if (attributes & FILE_ATTRIBUTE_DIRECTORY) { - // Skip '.' and '..' directories. - if (wcscmp(findData.cFileName, L".") == 0 || wcscmp(findData.cFileName, L"..") == 0) - { - continue; - } - - // MAX_PATH is enough here since the installer should not be using longer paths. - // No need to handle extended-length paths (\\?\) in this context. - WCHAR fullPath[MAX_PATH]; - hr = StringCchPrintfW(fullPath, MAX_PATH, L"%s\\%s", path, findData.cFileName); - if (FAILED(hr)) { - WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Path too long for item '%ls' in '%ls', skipping.", findData.cFileName, path); - continue; - } - - // Before acting, ensure the read-only attribute is not set. - if (findData.dwFileAttributes & FILE_ATTRIBUTE_READONLY) - { - if (FALSE == SetFileAttributesW(fullPath, findData.dwFileAttributes & ~FILE_ATTRIBUTE_READONLY)) - { - WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Failed to remove read-only attribute. Error: %lu", GetLastError()); - } - } - - if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) - { - // Check for reparse points (symlinks/junctions) to prevent directory traversal attacks. - // Do not follow reparse points, only remove the link itself. - if (findData.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) - { - WcaLog(LOGMSG_STANDARD, "RecursiveDelete: Not recursing into reparse point (symlink/junction), deleting link itself: %ls", fullPath); - SafeDeleteItem(fullPath); - } - else - { - // Recursively delete directory contents first - RecursiveDelete(fullPath); - // Then delete the directory itself - SafeDeleteItem(fullPath); - } - } - else - { - // Delete file using safe handle-based deletion - SafeDeleteItem(fullPath); - } - } while (FindNextFileW(hFind, &findData) != 0); - - DWORD lastError = GetLastError(); - if (lastError != ERROR_NO_MORE_FILES) - { - WcaLog(LOGMSG_STANDARD, "RecursiveDelete: FindNextFileW failed with error %lu", lastError); + WcaLog(LOGMSG_STANDARD, "Runtime cleanup skipped directory '%ls'.", fullPath); + return FALSE; } - FindClose(hFind); + ClearReadOnlyAttribute(fullPath, attributes); + WcaLog(LOGMSG_STANDARD, "Runtime cleanup deleting '%ls'.", fullPath); + return SafeDeleteItem(fullPath); } // See `Package.wxs` for the sequence of this custom action. @@ -178,13 +169,13 @@ void RecursiveDelete(LPCWSTR path) // 2. RemoveExistingProducts // ├─ TerminateProcesses // ├─ TryStopDeleteService -// ├─ RemoveInstallFolder - <-- Here +// ├─ RemoveRuntimeGeneratedFiles - <-- Here // └─ RemoveFiles // 3. InstallValidate // 4. InstallFiles // 5. InstallExecute // 6. InstallFinalize -UINT __stdcall RemoveInstallFolder( +UINT __stdcall RemoveRuntimeGeneratedFiles( __in MSIHANDLE hInstall) { HRESULT hr = S_OK; @@ -194,7 +185,7 @@ UINT __stdcall RemoveInstallFolder( LPWSTR pwz = NULL; LPWSTR pwzData = NULL; - hr = WcaInitialize(hInstall, "RemoveInstallFolder"); + hr = WcaInitialize(hInstall, "RemoveRuntimeGeneratedFiles"); ExitOnFailure(hr, "Failed to initialize"); hr = WcaGetProperty(L"CustomActionData", &pwzData); @@ -202,24 +193,20 @@ UINT __stdcall RemoveInstallFolder( pwz = pwzData; hr = WcaReadStringFromCaData(&pwz, &installFolder); - ExitOnFailure(hr, "failed to read database key from custom action data: %ls", pwz); + ExitOnFailure(hr, "failed to read install folder from custom action data: %ls", pwz); if (installFolder == NULL || installFolder[0] == L'\0') { - WcaLog(LOGMSG_STANDARD, "Install folder path is empty, skipping recursive delete."); + WcaLog(LOGMSG_STANDARD, "Install folder path is empty, skipping runtime cleanup."); goto LExit; } if (PathIsRootW(installFolder)) { - WcaLog(LOGMSG_STANDARD, "Refusing to recursively delete root folder '%ls'.", installFolder); + WcaLog(LOGMSG_STANDARD, "Refusing runtime cleanup in root folder '%ls'.", installFolder); goto LExit; } - WcaLog(LOGMSG_STANDARD, "Attempting to recursively delete contents of install folder: %ls", installFolder); - - RecursiveDelete(installFolder); - - // The standard MSI 'RemoveFolders' action will take care of removing the (now empty) directories. - // We don't need to call RemoveDirectoryW on installFolder itself, as it might still be in use by the installer. + WcaLog(LOGMSG_STANDARD, "Removing runtime-generated files from install folder: %ls", installFolder); + DeleteRuntimeGeneratedFile(installFolder, L"RuntimeBroker_rustdesk.exe"); LExit: ReleaseStr(pwzData); diff --git a/res/msi/CustomActions/CustomActions.def b/res/msi/CustomActions/CustomActions.def index 01b03490c..d50fbf59b 100644 --- a/res/msi/CustomActions/CustomActions.def +++ b/res/msi/CustomActions/CustomActions.def @@ -2,7 +2,7 @@ LIBRARY "CustomActions" EXPORTS CustomActionHello - RemoveInstallFolder + RemoveRuntimeGeneratedFiles TerminateProcesses AddFirewallRules SetPropertyIsServiceRunning diff --git a/res/msi/Package/Components/Folders.wxs b/res/msi/Package/Components/Folders.wxs index de9edb7f3..6911600e9 100644 --- a/res/msi/Package/Components/Folders.wxs +++ b/res/msi/Package/Components/Folders.wxs @@ -16,8 +16,15 @@ - - + + + + + + + + + diff --git a/res/msi/Package/Components/RustDesk.wxs b/res/msi/Package/Components/RustDesk.wxs index 337e84ec3..952172bdc 100644 --- a/res/msi/Package/Components/RustDesk.wxs +++ b/res/msi/Package/Components/RustDesk.wxs @@ -12,7 +12,7 @@ - + @@ -77,21 +77,21 @@ - - - + + + - + - + - + - + diff --git a/res/msi/Package/Fragments/CustomActions.wxs b/res/msi/Package/Fragments/CustomActions.wxs index 3727c0dd3..3a9811eb8 100644 --- a/res/msi/Package/Fragments/CustomActions.wxs +++ b/res/msi/Package/Fragments/CustomActions.wxs @@ -5,7 +5,7 @@ - + diff --git a/res/msi/Package/UI/MyInstallDlg.wxs b/res/msi/Package/UI/MyInstallDlg.wxs index bf59d569c..06c37097c 100644 --- a/res/msi/Package/UI/MyInstallDlg.wxs +++ b/res/msi/Package/UI/MyInstallDlg.wxs @@ -23,12 +23,13 @@ Patch dialog sequence: --> + - + @@ -64,9 +65,16 @@ Patch dialog sequence: - - - + + + + + + + + + + From b6caa1a7b2bb72c02f5b24fa7709eaf34e56daaf Mon Sep 17 00:00:00 2001 From: John Fowler Date: Wed, 13 May 2026 08:59:29 +0200 Subject: [PATCH 264/277] hu.rs update (#14983) Translate a new string. --- src/lang/hu.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 7f9b3299e..b4cbc1f23 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -743,6 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Kijelző név"), ("password-hidden-tip", "Állandó jelszó lett beállítva (rejtett)."), ("preset-password-in-use-tip", "Jelenleg az alapértelmezett jelszót használja."), - ("Enable privacy mode", ""), + ("Enable privacy mode", "Adatvédelmi mód aktiválása"), ].iter().cloned().collect(); } From fe5a8cb2ad2d6b03a1e5bad42078c7153c582cef Mon Sep 17 00:00:00 2001 From: Alex Rijckaert Date: Wed, 13 May 2026 08:59:48 +0200 Subject: [PATCH 265/277] Update Dutch translation (#14984) --- src/lang/nl.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 833c947cf..5a68d756d 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -743,6 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Naam Weergeven"), ("password-hidden-tip", "Er is een permanent wachtwoord ingesteld (verborgen)."), ("preset-password-in-use-tip", "Het basis wachtwoord is momenteel in gebruik."), - ("Enable privacy mode", ""), + ("Enable privacy mode", "Schakel privacymodus in"), ].iter().cloned().collect(); } From dd265dadd79151d62f382a08110ce4db629bc862 Mon Sep 17 00:00:00 2001 From: rustdesk Date: Wed, 13 May 2026 18:08:08 +0800 Subject: [PATCH 266/277] update hbb_common --- libs/hbb_common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/hbb_common b/libs/hbb_common index 42af0f0ae..c8cbb6be2 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit 42af0f0aed0bb5fd5df4ff95fd4cc9816fcf5769 +Subproject commit c8cbb6be283e9215da87625016fe8838dda76c02 From 0d40cf2101a99ddae8edabf699eb71c50f41631a Mon Sep 17 00:00:00 2001 From: Alex Rijckaert Date: Thu, 14 May 2026 10:43:40 +0200 Subject: [PATCH 267/277] Update Dutch translations (#15024) Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> --- src/lang/nl.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 5a68d756d..0f91d6a61 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -743,6 +743,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "Naam Weergeven"), ("password-hidden-tip", "Er is een permanent wachtwoord ingesteld (verborgen)."), ("preset-password-in-use-tip", "Het basis wachtwoord is momenteel in gebruik."), - ("Enable privacy mode", "Schakel privacymodus in"), + ("Enable privacy mode", "Privacymodus inschakelen"), ].iter().cloned().collect(); } From 701a9c6cdc1df2210d8c3f954efa8545388d97b1 Mon Sep 17 00:00:00 2001 From: flusheDData <116861809+flusheDData@users.noreply.github.com> Date: Fri, 15 May 2026 09:31:25 +0200 Subject: [PATCH 268/277] New terms added (#15036) * Update es.rs New terms added * Update es.rs New terms added * Update Spanish translations for various strings * Fix typo in Spanish translation for TLS fallback * Add Spanish translations for various UI elements * Update es.rs --------- Co-authored-by: RustDesk <71636191+rustdesk@users.noreply.github.com> --- src/lang/es.rs | 88 +++++++++++++++++++++++++------------------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 2e543c25e..11c395f7d 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -74,7 +74,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Wrong Password", "Contraseña incorrecta"), ("Do you want to enter again?", "¿Quieres volver a entrar?"), ("Connection Error", "Error de conexión"), - ("Error", ""), + ("Error", ), ("Reset by the peer", "Restablecido por el par"), ("Connecting...", "Conectando..."), ("Connection in progress. Please wait.", "Conexión en curso. Espere por favor."), @@ -90,7 +90,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Receive", "Recibir"), ("Send", "Enviar"), ("Refresh File", "Actualizar archivo"), - ("Local", ""), + ("Local", ), ("Remote", "Remoto"), ("Remote Computer", "Computadora remota"), ("Local Computer", "Computadora local"), @@ -208,7 +208,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Cerrado manualmente por el par"), ("Enable remote configuration modification", "Habilitar modificación remota de configuración"), ("Run without install", "Ejecutar sin instalar"), - ("Connect via relay", ""), + ("Connect via relay", "Conectar a través de relay"), ("Always connect via relay", "Conéctese siempre a través de relay"), ("whitelist_tip", "Solo las direcciones IP autorizadas pueden conectarse a este escritorio"), ("Login", "Iniciar sesión"), @@ -228,7 +228,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Username missed", "Olvidó su nombre de usuario"), ("Password missed", "Olvidó su contraseña"), ("Wrong credentials", "Credenciales incorrectas"), - ("The verification code is incorrect or has expired", ""), + ("The verification code is incorrect or has expired", "El código de verificación es incorrecto o ha caducado"), ("Edit Tag", "Editar tag"), ("Forget Password", "Olvidar contraseña"), ("Favorites", "Favoritos"), @@ -302,8 +302,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Dejar RustDesk como Servicio en 2do plano"), ("Ignore Battery Optimizations", "Ignorar optimizacioens de bateria"), ("android_open_battery_optimizations_tip", "Si deseas deshabilitar esta característica, por favor, ve a la página siguiente de ajustes, busca y entra en [Batería] y desmarca [Sin restricción]"), - ("Start on boot", ""), - ("Start the screen sharing service on boot, requires special permissions", ""), + ("Start on boot", "Iniciar al arrancar"), + ("Start the screen sharing service on boot, requires special permissions", "Iniciar el servicio de pantalla compartida al arrancar, requiere permisos especiales"), ("Connection not allowed", "Conexión no disponible"), ("Legacy mode", "Modo heredado"), ("Map mode", "Modo mapa"), @@ -326,8 +326,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Relación"), ("Image Quality", "Calidad de imagen"), ("Scroll Style", "Estilo de desplazamiento"), - ("Show Toolbar", ""), - ("Hide Toolbar", ""), + ("Show Toolbar", "Mostrar herramientas"), + ("Hide Toolbar", "Ocultar herramientas"), ("Direct Connection", "Conexión directa"), ("Relay Connection", "Conexión Relay"), ("Secure Connection", "Conexión segura"), @@ -338,7 +338,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Security", "Seguridad"), ("Theme", "Tema"), ("Dark Theme", "Tema Oscuro"), - ("Light Theme", ""), + ("Light Theme", "Tema claro"), ("Dark", "Oscuro"), ("Light", "Claro"), ("Follow System", "Tema del sistema"), @@ -355,12 +355,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Audio Input Device", "Dispositivo de entrada de audio"), ("Use IP Whitelisting", "Usar lista de IPs admitidas"), ("Network", "Red"), - ("Pin Toolbar", ""), - ("Unpin Toolbar", ""), + ("Pin Toolbar", "Anclar herramientas"), + ("Unpin Toolbar", "Desanclar herramientas"), ("Recording", "Grabando"), ("Directory", "Directorio"), ("Automatically record incoming sessions", "Grabación automática de sesiones entrantes"), - ("Automatically record outgoing sessions", ""), + ("Automatically record outgoing sessions", "Grabación automática de sesiones salientes"), ("Change", "Cambiar"), ("Start session recording", "Comenzar grabación de sesión"), ("Stop session recording", "Detener grabación de sesión"), @@ -368,7 +368,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable LAN discovery", "Habilitar descubrimiento de LAN"), ("Deny LAN discovery", "Denegar descubrimiento de LAN"), ("Write a message", "Escribir un mensaje"), - ("Prompt", ""), + ("Prompt", "Solicitud"), ("Please wait for confirmation of UAC...", "Por favor, espera confirmación de UAC"), ("elevated_foreground_window_tip", "La ventana actual del escritorio remoto necesita privilegios elevados para funcionar, así que no puedes usar ratón y teclado temporalmente. Puedes solicitar al usuario remoto que minimize la ventana actual o hacer clic en el botón de elevación de la ventana de gestión de conexión. Para evitar este problema, se recomienda instalar el programa en el dispositivo remto."), ("Disconnected", "Desconectado"), @@ -616,9 +616,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("During service is on", "Mientras el servicio está activo"), ("Capture screen using DirectX", "Capturar pantalla con DirectX"), ("Back", "Atrás"), - ("Apps", ""), - ("Volume up", "Bajar volumen"), - ("Volume down", "Subir volumen"), + ("Apps", "Aplicaciones"), + ("Volume up", "Subir volumen"), + ("Volume down", "Bajar volumen"), ("Power", "Encendido"), ("Telegram bot", "Bot de Telegram"), ("enable-bot-tip", "Si activas esta característica puedes recibir código 2FA de tu bot. También puede funcionar como notificación de conexión."), @@ -651,7 +651,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", "Actualizar portapapeles del cliente"), ("Untagged", "Sin itiquetar"), ("new-version-of-{}-tip", "Hay una nueva versión de {} disponible"), - ("Accessible devices", ""), + ("Accessible devices", "Dispositivos accesibles"), ("upgrade_remote_rustdesk_client_to_{}_tip", "Por favor, actualiza el cliente RustDesk a la versión {} o superior en el lado remoto"), ("d3d_render_tip", "Al activar el renderizado D3D, la pantalla de control remoto puede verse negra en algunos equipos."), ("Use D3D rendering", "Usar renderizado D3D"), @@ -689,9 +689,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use WebSocket", "Usar WebSocket"), ("Trackpad speed", "Velocidad de trackpad"), ("Default trackpad speed", "Velocidad predeterminada de trackpad"), - ("Numeric one-time password", ""), - ("Enable IPv6 P2P connection", ""), - ("Enable UDP hole punching", ""), + ("Numeric one-time password", "Contraseña numérica de un solo uso"), + ("Enable IPv6 P2P connection", "Habilitar conexión IPv6 P2P"), + ("Enable UDP hole punching", "Habilitar perforación de agujero UDP"), ("View camera", "Ver cámara"), ("Enable camera", "Habilitar cámara"), ("No cameras", "No hay cámaras"), @@ -708,8 +708,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to check if the user is an administrator.", "No se ha podido comprobar si el usuario es un administrador."), ("Supported only in the installed version.", "Soportado solo en la versión instalada."), ("elevation_username_tip", "Introduzca el nombre de usuario o dominio\\NombreDeUsuario"), - ("Preparing for installation ...", ""), - ("Show my cursor", ""), + ("Preparing for installation ...", "Preparando instlación..."), + ("Show my cursor", "Mostrar mi cursor"), ("Scale custom", "Escala personalizada"), ("Custom scale slider", "Control deslizante de escala personalizada"), ("Decrease", "Disminuir"), @@ -721,28 +721,28 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show virtual joystick", "Mostrar joystick virtual"), ("Edit note", "Editar nota"), ("Alias", ""), - ("ScrollEdge", ""), - ("Allow insecure TLS fallback", ""), - ("allow-insecure-tls-fallback-tip", ""), - ("Disable UDP", ""), - ("disable-udp-tip", ""), - ("server-oss-not-support-tip", ""), - ("input note here", ""), - ("note-at-conn-end-tip", ""), - ("Show terminal extra keys", ""), - ("Relative mouse mode", ""), - ("rel-mouse-not-supported-peer-tip", ""), - ("rel-mouse-not-ready-tip", ""), - ("rel-mouse-lock-failed-tip", ""), - ("rel-mouse-exit-{}-tip", ""), - ("rel-mouse-permission-lost-tip", ""), - ("Changelog", ""), - ("keep-awake-during-outgoing-sessions-label", ""), - ("keep-awake-during-incoming-sessions-label", ""), + ("ScrollEdge", "Desplazamiento de pantalla"), + ("Allow insecure TLS fallback", "Permitir conexión TLS insegura de respaldo"), + ("allow-insecure-tls-fallback-tip", "De forma predeterminada, RustDesk verifica el certificado de servidor para protocolos que usen TLS.\nCon esta opción habilitada, Rustdesk volverá al paso de omisión de verificación y procederá en caso de fallo de verificación."), + ("Disable UDP", "Inhabilitar UDP"), + ("disable-udp-tip", "Controla si se usa TCP solamente.\nCuando esta opción está activa, RustDesk no usará más el puerto UDP 21116, en su lugar se usará el TCP 21116."), + ("server-oss-not-support-tip", "NOTA: El servidor RustDesk OSS no incluye esta característica."), + ("input note here", "Introducir nota aquí"), + ("note-at-conn-end-tip", "Pedir nota al finalizar la conexión"), + ("Show terminal extra keys", "Mostrar teclas extra del terminal"), + ("Relative mouse mode", "Modo de ratón relativo"), + ("rel-mouse-not-supported-peer-tip", "El modo relativo de ratón no está soportado por el par."), + ("rel-mouse-not-ready-tip", "El modo relativo de ratón aún no está preparado. Por favor, inténtalo de nuevo."), + ("rel-mouse-lock-failed-tip", "Ha fallado el bloqueo del cursor. El modo relativo del ratón ha sido inhabilitado."), + ("rel-mouse-exit-{}-tip", "Pulsa {} para salir."), + ("rel-mouse-permission-lost-tip", "Permiso de teclado revocado. El modo relativo del ratón ha sido inhabilitado."), + ("Changelog", "Registro de cambios"), + ("keep-awake-during-outgoing-sessions-label", "Mantener la pantalla activa durante sesiones salientes"), + ("keep-awake-during-incoming-sessions-label", "Mantener la pantalla activa durante sesiones entrantes"), ("Continue with {}", "Continuar con {}"), - ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), - ("Enable privacy mode", ""), + ("Display Name", "Nombre de pantalla"), + ("password-hidden-tip", "La contraseña permanente está ajustada a (oculta)."), + ("preset-password-in-use-tip", "Se está usando la contraseña predeterminada."), + ("Enable privacy mode", "Habilitar modo privado"), ].iter().cloned().collect(); } From 9f8f726f12da733527cffafd8fa9657c4784c2af Mon Sep 17 00:00:00 2001 From: rustdesk Date: Fri, 15 May 2026 17:30:59 +0800 Subject: [PATCH 269/277] fix compile --- src/lang/es.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lang/es.rs b/src/lang/es.rs index 11c395f7d..b822432a0 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -74,7 +74,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Wrong Password", "Contraseña incorrecta"), ("Do you want to enter again?", "¿Quieres volver a entrar?"), ("Connection Error", "Error de conexión"), - ("Error", ), + ("Error", ""), ("Reset by the peer", "Restablecido por el par"), ("Connecting...", "Conectando..."), ("Connection in progress. Please wait.", "Conexión en curso. Espere por favor."), @@ -90,7 +90,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Receive", "Recibir"), ("Send", "Enviar"), ("Refresh File", "Actualizar archivo"), - ("Local", ), + ("Local", ""), ("Remote", "Remoto"), ("Remote Computer", "Computadora remota"), ("Local Computer", "Computadora local"), From 472c4fc03ab3e7e160bfe71d46d5481c8946f9bb Mon Sep 17 00:00:00 2001 From: RustDesk <71636191+rustdesk@users.noreply.github.com> Date: Sat, 16 May 2026 14:41:34 +0800 Subject: [PATCH 270/277] --deploy, reuse the device token (#15035) * --deploy, reuse the device token * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix review * no id validation in deploy, so to keep the same behavior in udp register pk * Fix collapsed toolbar drag preview sizing * Revert "Fix collapsed toolbar drag preview sizing" This reverts commit 66e39abb740b8cebbbf04e0441ce0c7433272d99. * remove too many logs --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/core_main.rs | 92 ++++++++++++++++++++++++++++++++++++++ src/ipc.rs | 12 +++++ src/rendezvous_mediator.rs | 59 ++++++++++++++++++++++++ 3 files changed, 163 insertions(+) diff --git a/src/core_main.rs b/src/core_main.rs index 67a83a37e..a0ca5eb95 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -627,6 +627,98 @@ pub fn core_main() -> Option> { println!("Installation and administrative privileges required!"); } return None; + } else if args[0] == "--deploy" { + if config::Config::no_register_device() { + println!("Cannot deploy an unregistrable device!"); + } else if crate::platform::is_installed() && is_root() { + let max = args.len() - 1; + let pos = args.iter().position(|x| x == "--token").unwrap_or(max); + if pos >= max { + println!("--token is required!"); + return None; + } + let token = args[pos + 1].to_owned(); + let get_value = |c: &str| { + let pos = args.iter().position(|x| x == c).unwrap_or(max); + if pos < max { + Some(args[pos + 1].to_owned()) + } else { + None + } + }; + let new_id = get_value("--id"); + let local_id = crate::ipc::get_id(); + let id_to_deploy = new_id.clone().unwrap_or_else(|| local_id.clone()); + let uuid = crate::encode64(hbb_common::get_uuid()); + let pk = crate::encode64( + hbb_common::config::Config::get_key_pair().1, + ); + let body = serde_json::json!({ + "id": id_to_deploy, + "uuid": uuid, + "pk": pk, + }); + let header = "Authorization: Bearer ".to_owned() + &token; + let url = crate::ui_interface::get_api_server() + "/api/devices/deploy"; + match crate::post_request_sync(url, body.to_string(), &header) { + Err(err) => { + println!("Request failed: {}", err); + std::process::exit(1); + } + Ok(text) => { + let parsed: serde_json::Value = + serde_json::from_str(&text).unwrap_or(serde_json::Value::Null); + let result = parsed["result"].as_str().unwrap_or(""); + match result { + "OK" => { + if let Some(ref new_id) = new_id { + if *new_id != local_id { + if let Err(err) = + crate::ipc::set_config("id", new_id.clone()) + { + println!( + "Failed to persist deployed id locally: {}", + err + ); + std::process::exit(1); + } + } + } + if let Err(err) = crate::ipc::notify_deployed() { + log::warn!("Failed to notify deployed state: {}", err); + } + println!("Device deployed."); + } + "NOT_ENABLED" => { + println!("Server does not require deployment."); + std::process::exit(3); + } + "INVALID_INPUT" => { + println!("Invalid input."); + std::process::exit(5); + } + "ID_TAKEN" => { + println!( + "Id `{}` is already used by another machine on the server.", + id_to_deploy + ); + std::process::exit(6); + } + _ => { + if text.is_empty() { + println!("Unknown response."); + } else { + println!("{}", text); + } + std::process::exit(1); + } + } + } + } + } else { + println!("Installation and administrative privileges required!"); + } + return None; } else if args[0] == "--check-hwcodec-config" { #[cfg(feature = "hwcodec")] crate::ipc::hwcodec_process(); diff --git a/src/ipc.rs b/src/ipc.rs index 0258a2816..0cd30634a 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -312,6 +312,7 @@ pub enum Data { ClipboardNonFile(Option<(String, Vec)>), PrivacyModeState((i32, PrivacyModeState, String)), TestRendezvousServer, + Deployed, #[cfg(not(any(target_os = "android", target_os = "ios")))] Keyboard(DataKeyboard), #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -929,6 +930,10 @@ async fn handle(data: Data, stream: &mut Connection) { Data::TestRendezvousServer => { crate::test_rendezvous_server(); } + Data::Deployed => { + crate::rendezvous_mediator::NEEDS_DEPLOY.store(false, Ordering::SeqCst); + crate::rendezvous_mediator::RendezvousMediator::restart(); + } #[cfg(feature = "flutter")] #[cfg(not(any(target_os = "android", target_os = "ios")))] Data::SwitchSidesRequest(id) => { @@ -1737,6 +1742,13 @@ pub async fn test_rendezvous_server() -> ResultType<()> { Ok(()) } +#[tokio::main(flavor = "current_thread")] +pub async fn notify_deployed() -> ResultType<()> { + let mut c = connect(1000, "").await?; + c.send(&Data::Deployed).await?; + Ok(()) +} + #[tokio::main(flavor = "current_thread")] pub async fn send_url_scheme(url: String) -> ResultType<()> { connect(1_000, "_url") diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 3ef280a2a..89d7fa01e 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -41,6 +41,30 @@ lazy_static::lazy_static! { static SHOULD_EXIT: AtomicBool = AtomicBool::new(false); static MANUAL_RESTARTED: AtomicBool = AtomicBool::new(false); static SENT_REGISTER_PK: AtomicBool = AtomicBool::new(false); +pub(crate) static NEEDS_DEPLOY: AtomicBool = AtomicBool::new(false); +// register_pk retry interval (ms) when device is awaiting deployment +const DEPLOY_RETRY_INTERVAL: i64 = 30_000; +lazy_static::lazy_static! { + static ref LAST_NOT_DEPLOYED_REGISTER: Mutex> = Mutex::new(None); +} + +// Single source of truth for the "awaiting deployment" backoff. The server has +// already told us this device is not in its db; until the operator runs +// `rustdesk --deploy --token ` there is no point re-running the +// register path more often than DEPLOY_RETRY_INTERVAL. Gating in the timer +// loops (rather than only inside register_pk) also avoids the +// last_register_sent / fails / latency / UDP-rebind churn the loop would +// otherwise spin on while no response ever comes back. +async fn deploy_register_throttled() -> bool { + if !NEEDS_DEPLOY.load(Ordering::SeqCst) { + return false; + } + LAST_NOT_DEPLOYED_REGISTER + .lock() + .await + .map(|t| (t.elapsed().as_millis() as i64) < DEPLOY_RETRY_INTERVAL) + .unwrap_or(false) +} #[derive(Clone)] pub struct RendezvousMediator { @@ -226,6 +250,14 @@ impl RendezvousMediator { if SHOULD_EXIT.load(Ordering::SeqCst) { break; } + // The server already told us this device is not deployed. Skip + // the whole register / fails / latency / UDP-rebind path until + // DEPLOY_RETRY_INTERVAL elapses, otherwise the loop spins every + // few seconds (log spam + misapplied network-recovery rebind) + // until the operator runs `rustdesk --deploy`. + if deploy_register_throttled().await { + continue; + } let now = Some(Instant::now()); let expired = last_register_resp.map(|x| x.elapsed().as_millis() as i64 >= REG_INTERVAL).unwrap_or(true); let timeout = last_register_sent.map(|x| x.elapsed().as_millis() as i64 >= reg_timeout).unwrap_or(false); @@ -289,10 +321,22 @@ impl RendezvousMediator { Config::set_key_confirmed(true); Config::set_host_key_confirmed(&self.host_prefix, true); *SOLVING_PK_MISMATCH.lock().await = "".to_owned(); + NEEDS_DEPLOY.store(false, Ordering::SeqCst); } Ok(register_pk_response::Result::UUID_MISMATCH) => { self.handle_uuid_mismatch(sink).await?; } + Ok(register_pk_response::Result::NOT_DEPLOYED) => { + if !NEEDS_DEPLOY.load(Ordering::SeqCst) { + log::warn!("Server requires deployment. Run `rustdesk --deploy --token ` on this device."); + } + NEEDS_DEPLOY.store(true, Ordering::SeqCst); + // Clear key_confirmed so the UI reflects the truth: this device is + // not currently registered. Covers the case where an online device + // was deleted by an admin while running. + Config::set_key_confirmed(false); + Config::set_host_key_confirmed(&self.host_prefix, false); + } _ => { log::error!("unknown RegisterPkResponse"); } @@ -678,6 +722,21 @@ impl RendezvousMediator { } async fn register_pk(&mut self, socket: Sink<'_>) -> ResultType<()> { + // Throttle register_pk when the device is awaiting deployment: server + // already told us we're not in its db; sending more often than every + // DEPLOY_RETRY_INTERVAL ms is wasted traffic until the operator runs + // `rustdesk --deploy --token `. + if NEEDS_DEPLOY.load(Ordering::SeqCst) { + let mut last = LAST_NOT_DEPLOYED_REGISTER.lock().await; + if let Some(t) = *last { + if (t.elapsed().as_millis() as i64) < DEPLOY_RETRY_INTERVAL { + return Ok(()); + } + } + *last = Some(Instant::now()); + } else { + *LAST_NOT_DEPLOYED_REGISTER.lock().await = None; + } let mut msg_out = Message::new(); let pk = Config::get_key_pair().1; let uuid = hbb_common::get_uuid(); From 377547fa1128823d6c5a4ea17b1310640264713b Mon Sep 17 00:00:00 2001 From: IronCodeStudios Date: Sun, 17 May 2026 16:02:23 +0800 Subject: [PATCH 271/277] scrap/wayland: insert videoconvert to fix screencast on COSMIC / DMA-BUF portals (#15063) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Wayland compositors whose xdg-desktop-portal backend exposes screencast frames as DMA-BUF buffers — notably xdg-desktop-portal-cosmic 0.1.0 on Pop!_OS 24.04 / COSMIC — inbound screen capture fails. PipeWireRecorder links pipewiresrc directly to an appsink whose caps only accept video/x-raw BGRx/RGBx in system memory. That format set is too narrow for the portal's buffer-type / modifier negotiation, which collapses with: pw.link: negotiating -> error no more output formats (-22) gstpipewiresrc: stream error: no more output formats gstbasesrc: streaming stopped, reason not-negotiated (-4) ERROR src/server/wayland.rs: Failed scrap Element failed to change its state Inserting a videoconvert element between pipewiresrc and appsink widens the negotiable format set to any system-memory video/x-raw format, giving the portal room to settle on a format it can deliver via its SHM path. videoconvert then converts to the BGRx/RGBx the appsink expects. Verified on Pop!_OS 24.04 / COSMIC with gst-launch, before and after: # fails (current behaviour): gst-launch-1.0 pipewiresrc path=N ! video/x-raw,format=BGRx ! fakesink # works (with this change): gst-launch-1.0 pipewiresrc path=N ! videoconvert ! video/x-raw,format=BGRx ! fakesink After the change, inbound connections capture and stream the desktop normally and the "Failed scrap" error no longer occurs. Co-authored-by: Claude Opus 4.7 (1M context) --- libs/scrap/src/wayland/pipewire.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index aedf786b7..8859d0d3b 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -276,12 +276,21 @@ impl PipeWireRecorder { // see: https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/982 src.set_property("always-copy", &true)?; + // COSMIC/Wayland fix: insert videoconvert between pipewiresrc and appsink. + // xdg-desktop-portal-cosmic's modifier negotiation fails when the downstream + // format set is too narrow (appsink only accepts BGRx/RGBx), producing + // "no more output formats" / not-negotiated (-4). videoconvert accepts any + // system-memory video/x-raw format, widening negotiation so the portal can + // settle on a format it can deliver via its SHM path. + let convert = gst::ElementFactory::make("videoconvert", None)?; + let sink = gst::ElementFactory::make("appsink", None)?; sink.set_property("drop", &true)?; sink.set_property("max-buffers", &1u32)?; - pipeline.add_many(&[&src, &sink])?; - src.link(&sink)?; + pipeline.add_many(&[&src, &convert, &sink])?; + src.link(&convert)?; + convert.link(&sink)?; let appsink = sink .dynamic_cast::() From bc2c36215d15dd2ec223a5d470e38ebc87b9de7d Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 18 May 2026 16:32:46 +0800 Subject: [PATCH 272/277] fix(ipc): scope active-user IPC routing to root CLI main requests (#15058) * fix(ipc): scope active-user IPC routing to root CLI main requests Signed-off-by: fufesou * fix(ipc): cmdline, comments fails close Signed-off-by: fufesou * fix(ipc): cmdline, better check Signed-off-by: fufesou * fix(ipc): cmdline, try active uid when no --server processes Signed-off-by: fufesou * fix(ipc): cmdline, select active uid Signed-off-by: fufesou * fix(ipc): remove unused import Signed-off-by: fufesou --------- Signed-off-by: fufesou --- libs/hbb_common | 2 +- src/core_main.rs | 63 ++++++++++++ src/ipc.rs | 210 +++++++++++++++++++++++++++++++++++++--- src/ipc/auth.rs | 71 +++++++++++--- src/platform/windows.rs | 1 + 5 files changed, 317 insertions(+), 30 deletions(-) diff --git a/libs/hbb_common b/libs/hbb_common index c8cbb6be2..9043c15ac 160000 --- a/libs/hbb_common +++ b/libs/hbb_common @@ -1 +1 @@ -Subproject commit c8cbb6be283e9215da87625016fe8838dda76c02 +Subproject commit 9043c15acc6d5b42b6c12ad284c16c1ec172f1f0 diff --git a/src/core_main.rs b/src/core_main.rs index a0ca5eb95..ee2a9d90d 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -199,6 +199,20 @@ pub fn core_main() -> Option> { } std::thread::spawn(move || crate::start_server(false, no_server)); } else { + #[cfg(any(target_os = "linux", target_os = "macos"))] + // Root CLI management commands must talk to the user `--server` main IPC. + // Example: `sudo rustdesk --option custom-rendezvous-server` should query the + // user's IPC instead of root's `/tmp/-0/ipc`; `connect()` still limits this + // routing to empty-postfix main IPC only. + let _user_main_ipc_scope = if crate::platform::is_installed() + && is_root() + && is_user_main_ipc_scope_cli_command(&args) + { + Some(crate::ipc::UserMainIpcScope::new()) + } else { + None + }; + #[cfg(windows)] { use crate::platform; @@ -938,6 +952,55 @@ fn is_root() -> bool { crate::platform::is_root() } +#[cfg(any(target_os = "linux", target_os = "macos", test))] +fn is_user_main_ipc_scope_cli_command(args: &[String]) -> bool { + matches!( + args.first().map(String::as_str), + Some("--password") + | Some("--set-unlock-pin") + | Some("--get-id") + | Some("--set-id") + | Some("--config") + | Some("--option") + | Some("--assign") + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn args(values: &[&str]) -> Vec { + values.iter().map(|value| value.to_string()).collect() + } + + #[test] + fn user_main_ipc_scope_cli_command_matches_management_commands_only() { + for command in [ + "--password", + "--set-unlock-pin", + "--get-id", + "--set-id", + "--config", + "--option", + "--assign", + ] { + assert!(is_user_main_ipc_scope_cli_command(&args(&[command]))); + } + + for command in [ + "--service", + "--server", + "--tray", + "--cm", + "--check-hwcodec-config", + "--connect", + ] { + assert!(!is_user_main_ipc_scope_cli_command(&args(&[command]))); + } + } +} + /// Check if the executable is a Quick Support version. /// Note: This function must be kept in sync with `libs/portable/src/main.rs`. #[cfg(windows)] diff --git a/src/ipc.rs b/src/ipc.rs index 0cd30634a..ffe1b08a5 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -33,25 +33,25 @@ use hbb_common::{ tokio_util::codec::Framed, ResultType, }; -#[cfg(any(target_os = "linux", target_os = "macos"))] -use ipc_auth::authorize_service_scoped_ipc_connection; #[cfg(windows)] pub(crate) use ipc_auth::authorize_windows_portable_service_ipc_connection; #[cfg(windows)] pub(crate) use ipc_auth::ensure_peer_executable_matches_current_by_pid_opt; #[cfg(windows)] pub(crate) use ipc_auth::log_rejected_windows_ipc_connection; -#[cfg(target_os = "linux")] -pub(crate) use ipc_auth::{ - active_uid, ensure_peer_executable_matches_current_by_fd, is_allowed_service_peer_uid, - log_rejected_uinput_connection, peer_uid_from_fd, -}; +#[cfg(any(target_os = "linux", target_os = "macos"))] +use ipc_auth::{active_uid, authorize_service_scoped_ipc_connection}; #[cfg(windows)] use ipc_auth::{ authorize_windows_main_ipc_connection, portable_service_listener_security_attributes, should_allow_everyone_create_on_windows, }; #[cfg(target_os = "linux")] +pub(crate) use ipc_auth::{ + ensure_peer_executable_matches_current_by_fd, is_allowed_service_peer_uid, + log_rejected_uinput_connection, peer_uid_from_fd, +}; +#[cfg(target_os = "linux")] use ipc_fs::terminal_count_candidate_uids; #[cfg(any(target_os = "linux", target_os = "macos"))] use ipc_fs::{ @@ -63,6 +63,8 @@ use parity_tokio_ipc::{ }; use serde_derive::{Deserialize, Serialize}; #[cfg(any(target_os = "linux", target_os = "macos"))] +use std::cell::Cell; +#[cfg(any(target_os = "linux", target_os = "macos"))] use std::os::unix::fs::PermissionsExt; use std::{ collections::HashMap, @@ -71,12 +73,47 @@ use std::{ // IPC actions here. pub const IPC_ACTION_CLOSE: &str = "close"; +#[cfg(target_os = "windows")] const PORTABLE_SERVICE_IPC_HANDSHAKE_TIMEOUT_MS: u64 = 3_000; +#[cfg(target_os = "windows")] pub(crate) const IPC_TOKEN_LEN: usize = 64; +#[cfg(target_os = "windows")] const IPC_TOKEN_RANDOM_BYTES: usize = IPC_TOKEN_LEN / 2; +#[cfg(target_os = "windows")] const _: () = assert!(IPC_TOKEN_LEN % 2 == 0); pub static EXIT_RECV_CLOSE: AtomicBool = AtomicBool::new(true); +#[cfg(any(target_os = "linux", target_os = "macos"))] +thread_local! { + static USE_USER_MAIN_IPC: Cell = Cell::new(false); +} + +#[must_use = "bind this guard to a local variable to keep the IPC scope active"] +/// Thread-local guard for routing root main IPC to the active user on Linux/macOS. +#[cfg(any(target_os = "linux", target_os = "macos"))] +pub(crate) struct UserMainIpcScope { + previous: bool, +} + +#[cfg(any(target_os = "linux", target_os = "macos"))] +impl UserMainIpcScope { + pub(crate) fn new() -> Self { + let previous = USE_USER_MAIN_IPC.with(|use_user_main| { + let previous = use_user_main.get(); + use_user_main.set(true); + previous + }); + Self { previous } + } +} + +#[cfg(any(target_os = "linux", target_os = "macos"))] +impl Drop for UserMainIpcScope { + fn drop(&mut self) { + USE_USER_MAIN_IPC.with(|use_user_main| use_user_main.set(self.previous)); + } +} + #[inline] pub async fn connect_service(ms_timeout: u64) -> ResultType> { connect(ms_timeout, crate::POSTFIX_SERVICE).await @@ -1112,11 +1149,7 @@ async fn handle(data: Data, stream: &mut Connection) { }; } -pub async fn connect(ms_timeout: u64, postfix: &str) -> ResultType> { - let path = Config::ipc_path(postfix); - connect_with_path(ms_timeout, &path).await -} - +#[cfg(target_os = "windows")] pub(crate) fn generate_one_time_ipc_token() -> ResultType { use hbb_common::rand::{rngs::OsRng, RngCore as _}; use std::fmt::Write as _; @@ -1137,6 +1170,7 @@ pub(crate) fn generate_one_time_ipc_token() -> ResultType { Ok(token) } +#[cfg(target_os = "windows")] pub(crate) fn constant_time_ipc_token_eq(expected: &str, candidate: &str) -> bool { if expected.len() != IPC_TOKEN_LEN || candidate.len() != IPC_TOKEN_LEN { return false; @@ -1149,6 +1183,7 @@ pub(crate) fn constant_time_ipc_token_eq(expected: &str, candidate: &str) -> boo == 0 } +#[cfg(target_os = "windows")] pub(crate) async fn portable_service_ipc_handshake_as_client( stream: &mut ConnectionTmpl, token: &str, @@ -1173,6 +1208,7 @@ where } } +#[cfg(target_os = "windows")] pub(crate) async fn portable_service_ipc_handshake_as_server( stream: &mut ConnectionTmpl, mut validate_token: F, @@ -1209,6 +1245,103 @@ async fn connect_with_path(ms_timeout: u64, path: &str) -> ResultType, + prefer_root: bool, +) -> ResultType { + let mut server_uids = server_uids.to_vec(); + server_uids.sort_unstable(); + server_uids.dedup(); + + match server_uids.as_slice() { + [] => { + if let Some(uid) = active_uid { + // If no `--server` processes are found but the active user is identifiable, + // try the active user anyway because the main process may also listen on "" IPC. + return Ok(uid); + } else { + bail!("No --server process found for user main IPC") + } + } + [uid] => return Ok(*uid), + _ => {} + } + + if prefer_root && server_uids.contains(&0) { + return Ok(0); + } + if let Some(active_uid) = active_uid.filter(|uid| server_uids.contains(uid)) { + return Ok(active_uid); + } + bail!("Multiple --server processes found for user main IPC"); +} + +#[cfg(any(target_os = "linux", target_os = "macos"))] +fn running_server_uids_for_current_exe() -> ResultType> { + let current_exe = std::env::current_exe()?; + let current_exe_path = std::fs::canonicalize(¤t_exe)?; + let current_pid = hbb_common::sysinfo::Pid::from_u32(std::process::id()); + let mut sys = hbb_common::sysinfo::System::new(); + sys.refresh_processes(); + let mut server_uids = Vec::new(); + for process in sys.processes().values() { + if process.pid() == current_pid { + continue; + } + if process.cmd().get(1).map_or(true, |arg| arg != "--server") { + continue; + } + let Ok(process_path) = std::fs::canonicalize(process.exe()) else { + continue; + }; + if process_path != current_exe_path { + continue; + } + let Some(uid) = process.user_id().map(|uid| **uid as u32) else { + // Root CLI management commands need a stable matching `--server` target. + // If this key process races during enumeration, failing the command is clearer + // than silently skipping it; `--server` is not expected to exit frequently. + bail!("Failed to read --server process uid"); + }; + server_uids.push(uid); + } + Ok(server_uids) +} + +#[cfg(any(target_os = "linux", target_os = "macos"))] +fn user_main_ipc_server_uid() -> ResultType { + let server_uids = running_server_uids_for_current_exe()?; + #[cfg(target_os = "linux")] + let prefer_root = crate::platform::linux::is_login_screen_wayland(); + #[cfg(target_os = "macos")] + let prefer_root = false; + select_server_uid_for_user_main_ipc(&server_uids, active_uid(), prefer_root) +} + +pub async fn connect(ms_timeout: u64, postfix: &str) -> ResultType> { + #[cfg(any(target_os = "linux", target_os = "macos"))] + { + let use_user_main_ipc = USE_USER_MAIN_IPC.with(|use_user_main| use_user_main.get()); + let is_root_main_ipc = + unsafe { hbb_common::libc::geteuid() == 0 } && postfix.is_empty() && use_user_main_ipc; + if is_root_main_ipc { + let uid = user_main_ipc_server_uid()?; + let path = Config::ipc_path_for_uid(uid, postfix); + return connect_with_path(ms_timeout, &path).await; + } + let path = Config::ipc_path(postfix); + return connect_with_path(ms_timeout, &path).await; + } + #[cfg(not(any(target_os = "linux", target_os = "macos")))] + { + let path = Config::ipc_path(postfix); + connect_with_path(ms_timeout, &path).await + } +} + #[cfg(target_os = "linux")] pub async fn connect_for_uid( ms_timeout: u64, @@ -2002,7 +2135,16 @@ mod test { assert!(std::mem::size_of::() <= 120); } - #[cfg(target_os = "linux")] + #[cfg(any(target_os = "linux", target_os = "macos"))] + #[test] + fn test_service_ipc_path_is_shared_across_uids() { + assert_eq!( + Config::ipc_path_for_uid(0, crate::POSTFIX_SERVICE), + Config::ipc_path_for_uid(501, crate::POSTFIX_SERVICE) + ); + } + + #[cfg(any(target_os = "linux", target_os = "macos"))] #[test] fn test_ipc_path_differs_by_uid_for_cm() { let effective_uid = unsafe { hbb_common::libc::geteuid() as u32 }; @@ -2021,4 +2163,46 @@ mod test { Config::ipc_path_for_uid(other_uid, postfix) ); } + + #[cfg(any(target_os = "linux", target_os = "macos"))] + #[test] + fn test_select_server_uid_uses_active_uid_when_no_server_found() { + assert_eq!( + select_server_uid_for_user_main_ipc(&[], Some(501), false).unwrap(), + 501 + ); + } + + #[cfg(any(target_os = "linux", target_os = "macos"))] + #[test] + fn test_select_server_uid_uses_single_server_uid() { + assert_eq!( + select_server_uid_for_user_main_ipc(&[501], None, false).unwrap(), + 501 + ); + } + + #[cfg(any(target_os = "linux", target_os = "macos"))] + #[test] + fn test_select_server_uid_prefers_active_uid_with_multiple_servers() { + assert_eq!( + select_server_uid_for_user_main_ipc(&[0, 501], Some(501), false).unwrap(), + 501 + ); + } + + #[cfg(any(target_os = "linux", target_os = "macos"))] + #[test] + fn test_select_server_uid_prefers_root_on_wayland_login_screen() { + assert_eq!( + select_server_uid_for_user_main_ipc(&[0, 501], Some(501), true).unwrap(), + 0 + ); + } + + #[cfg(any(target_os = "linux", target_os = "macos"))] + #[test] + fn test_select_server_uid_fails_when_multiple_servers_are_ambiguous() { + assert!(select_server_uid_for_user_main_ipc(&[501, 502], None, false).is_err()); + } } diff --git a/src/ipc/auth.rs b/src/ipc/auth.rs index 746a32eed..77fd148c6 100644 --- a/src/ipc/auth.rs +++ b/src/ipc/auth.rs @@ -607,27 +607,30 @@ pub(crate) fn log_rejected_windows_ipc_connection( peer_session_id: Option, expected_session_id: Option, peer_is_system: Option, + peer_is_elevated: Option, ) { static LOG_THROTTLE: OnceLock> = OnceLock::new(); throttled_unauthorized_ipc_log(&LOG_THROTTLE, |suppressed| { if suppressed > 0 { log::warn!( - "Rejected unauthorized connection on ipc channel: postfix={}, peer_pid={:?}, peer_session_id={:?}, expected_session_id={:?}, peer_is_system={:?} (suppressed {} similar events)", + "Rejected unauthorized connection on ipc channel: postfix={}, peer_pid={:?}, peer_session_id={:?}, expected_session_id={:?}, peer_is_system={:?}, peer_is_elevated={:?} (suppressed {} similar events)", postfix, peer_pid, peer_session_id, expected_session_id, peer_is_system, + peer_is_elevated, suppressed ); } else { log::warn!( - "Rejected unauthorized connection on ipc channel: postfix={}, peer_pid={:?}, peer_session_id={:?}, expected_session_id={:?}, peer_is_system={:?}", + "Rejected unauthorized connection on ipc channel: postfix={}, peer_pid={:?}, peer_session_id={:?}, expected_session_id={:?}, peer_is_system={:?}, peer_is_elevated={:?}", postfix, peer_pid, peer_session_id, expected_session_id, - peer_is_system + peer_is_system, + peer_is_elevated ); } }); @@ -655,8 +658,14 @@ pub(crate) fn authorize_service_scoped_ipc_connection(stream: &Connection, postf #[cfg(windows)] pub(crate) fn authorize_windows_main_ipc_connection(stream: &Connection, postfix: &str) -> bool { - let (authorized, peer_pid, peer_session_id, server_session_id, peer_is_system) = - stream.server_authorization_status(); + let ( + authorized, + peer_pid, + peer_session_id, + server_session_id, + peer_is_system, + peer_is_elevated, + ) = stream.server_authorization_status(); if !authorized { log_rejected_windows_ipc_connection( postfix, @@ -664,6 +673,7 @@ pub(crate) fn authorize_windows_main_ipc_connection(stream: &Connection, postfix peer_session_id, server_session_id, peer_is_system, + peer_is_elevated, ); return false; } @@ -776,7 +786,14 @@ impl ConnectionTmpl { fn server_authorization_status( &self, - ) -> (bool, Option, Option, Option, Option) { + ) -> ( + bool, + Option, + Option, + Option, + Option, + Option, + ) { let peer_pid = self.peer_pid(); let server_session_id = crate::platform::windows::get_current_process_session_id(); let peer_session_id = @@ -786,20 +803,34 @@ impl ConnectionTmpl { let peer_is_system = peer_is_system_result .as_ref() .and_then(|r| r.as_ref().ok().copied()); - if server_session_id.is_none() && !peer_is_system.unwrap_or(false) { - // When the server session id cannot be determined, the session-id allow-path is - // disabled and only SYSTEM peers can be authorized. - log::debug!( - "IPC authorization: server session id unavailable; rejecting non-SYSTEM peer, peer_pid={:?}, peer_session_id={:?}", - peer_pid, - peer_session_id - ); - } - let authorized = is_allowed_windows_session_scoped_peer( + let session_authorized = is_allowed_windows_session_scoped_peer( peer_is_system.unwrap_or(false), peer_session_id, server_session_id, ); + let peer_is_elevated_result = if session_authorized { + None + } else { + peer_pid.map(|pid| crate::platform::windows::is_elevated(Some(pid))) + }; + let peer_is_elevated = peer_is_elevated_result + .as_ref() + .and_then(|r| r.as_ref().ok().copied()); + if server_session_id.is_none() + && !peer_is_system.unwrap_or(false) + && !peer_is_elevated.unwrap_or(false) + { + // When the server session id cannot be determined, the session-id allow-path is + // disabled and only privileged peers can be authorized. + log::debug!( + "IPC authorization: server session id unavailable; rejecting non-privileged peer, peer_pid={:?}, peer_session_id={:?}", + peer_pid, + peer_session_id + ); + } + // Main IPC trusts same-session peers, LocalSystem, and elevated administrators. + // Service-scoped IPC channels keep their own stricter authorization paths. + let authorized = session_authorized || peer_is_elevated.unwrap_or(false); if !authorized { if let (Some(pid), Some(Err(err))) = (peer_pid, peer_is_system_result.as_ref()) { log::debug!( @@ -808,6 +839,13 @@ impl ConnectionTmpl { err ); } + if let (Some(pid), Some(Err(err))) = (peer_pid, peer_is_elevated_result.as_ref()) { + log::debug!( + "Failed to determine whether peer process is elevated, pid={}, err={}", + pid, + err + ); + } } ( authorized, @@ -815,6 +853,7 @@ impl ConnectionTmpl { peer_session_id, server_session_id, peer_is_system, + peer_is_elevated, ) } diff --git a/src/platform/windows.rs b/src/platform/windows.rs index a755714f9..1dc4a788a 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -614,6 +614,7 @@ fn authorize_service_scoped_ipc_connection( peer_session_id, expected_active_session_id, peer_is_system, + None, ); return false; } From 78e8134ad56094f58c53eaff2edae07a3845da55 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 18 May 2026 16:52:22 +0800 Subject: [PATCH 273/277] fix(ipc): cmdline, use scope, deploy (#15068) Signed-off-by: fufesou --- src/core_main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core_main.rs b/src/core_main.rs index ee2a9d90d..c9c1a658f 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -963,6 +963,7 @@ fn is_user_main_ipc_scope_cli_command(args: &[String]) -> bool { | Some("--config") | Some("--option") | Some("--assign") + | Some("--deploy") ) } From bb51c6aa4207b53904a2528615c5b94a37ecc053 Mon Sep 17 00:00:00 2001 From: fufesou Date: Mon, 18 May 2026 17:03:04 +0800 Subject: [PATCH 274/277] fix(ipc): cmdline, unit tests (#15069) Signed-off-by: fufesou --- src/core_main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core_main.rs b/src/core_main.rs index c9c1a658f..4515faa6b 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -985,6 +985,7 @@ mod tests { "--config", "--option", "--assign", + "--deploy", ] { assert!(is_user_main_ipc_scope_cli_command(&args(&[command]))); } From 546e9f1702572c4d5c9ce0d0f977cea17ba53c8a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 May 2026 09:07:43 +0800 Subject: [PATCH 275/277] Git submodule: Bump libs/hbb_common from `c8cbb6b` to `9043c15` (#15067) Bumps [libs/hbb_common](https://github.com/rustdesk/hbb_common) from `c8cbb6b` to `9043c15`. - [Release notes](https://github.com/rustdesk/hbb_common/releases) - [Commits](https://github.com/rustdesk/hbb_common/compare/c8cbb6be283e9215da87625016fe8838dda76c02...9043c15acc6d5b42b6c12ad284c16c1ec172f1f0) --- updated-dependencies: - dependency-name: libs/hbb_common dependency-version: 9043c15acc6d5b42b6c12ad284c16c1ec172f1f0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From b81ae6c8949a50f64bac175f32217b8897078a2d Mon Sep 17 00:00:00 2001 From: Maison da Silva Date: Fri, 22 May 2026 07:36:15 -0300 Subject: [PATCH 276/277] Translate various labels to Portuguese-BR (#15086) Update --- src/lang/ptbr.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 4eb2c1544..36581d4f1 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -740,9 +740,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-outgoing-sessions-label", "Manter tela ativa durante sessões de saída"), ("keep-awake-during-incoming-sessions-label", "Manter tela ativa durante sessões de entrada"), ("Continue with {}", "Continuar com {}"), - ("Display Name", ""), - ("password-hidden-tip", ""), - ("preset-password-in-use-tip", ""), - ("Enable privacy mode", ""), + ("Display Name", "Nome de Exibição"), + ("password-hidden-tip", "A senha permanente está definida como (oculta)."), + ("preset-password-in-use-tip", "A senha predefinida está sendo usada."), + ("Enable privacy mode", "Habilitar modo de privacidade"), ].iter().cloned().collect(); } From 6ad56075d6d6b809f5699963bc417c48a138347c Mon Sep 17 00:00:00 2001 From: Luke <81411590+LukeCGG@users.noreply.github.com> Date: Sun, 24 May 2026 21:08:45 +1000 Subject: [PATCH 277/277] Drag whole toolbar; snap to all four edges of the remote session window (#15051) * Drag whole toolbar; snap to all four edges Today the drag handle on the remote-session toolbar repositions only the handle row -- the icons themselves stay centered at the top. This change applies the position to the entire toolbar wrapper so dragging the handle moves the whole thing, and extends snapping from top-only to any of the four window edges. When docked left/right the toolbar reflows vertically. A live ghost preview shows where the toolbar will land while you drag, with a small hysteresis bias to keep the preview from flickering near corners. The legacy 'remote-menubar-drag-x' session option is read as a fallback on first load so existing users keep their saved horizontal position; new option keys are 'remote-menubar-edge' and 'remote-menubar-frac'. Tested locally on Windows. macOS / Linux / web desktop use the same shared widget with no platform-specific calls, but I did not verify them. * Load edge independently and clamp loaded fraction Addresses CodeRabbit review on #15051: parse the saved edge regardless of whether the new fraction option is present so a partial write of frac doesn't reset the toolbar back to top, and clamp the loaded fraction to the kOptionRemoteMenubarDragLeft/Right contract so a corrupted or out-of-range saved value can't bypass the bounds until the user drags again. * Require edge activation zone to switch dock; preserve horizontal slide Per review feedback on #15051: nearest-edge-wins made a low-intent horizontal slide too easy to escalate into a high-impact orientation change (vertical reflow on left/right dock). The default drag now keeps the toolbar on its current dock edge and just updates the fraction along that edge -- the prior horizontal-slide behavior. An alternate edge is only previewed/committed when the cursor enters its 32 px activation zone; once previewed, the cursor has to move back 64 px before reverting (hysteresis at the zone boundary). * Gate multi-edge docking behind a settings toggle; default = horizontal slide Replaces the activation-zone approach with an explicit opt-in setting in Settings -> Other ("Allow docking remote toolbar to any window edge"). This addresses the concern that a low-intent horizontal drag shouldn't be able to trigger a high-impact orientation change, while still letting users who want multi-edge docking opt in cleanly. Default (toggle off): - The original horizontal slide is preserved. - The bug fix from the first commit still applies: dragging the handle moves the whole toolbar, and the position persists across collapse/expand (no more re-center on re-open). - Draggable is axis-locked to horizontal so the feedback widget stays on the top line during drag. Opt-in (toggle on): - Full nearest-edge wins with the live preview ghost and corner hysteresis; toolbar reflows vertically on left/right docks. - Draggable is unlocked for 2D drag. Reads the option via mainGetLocalBoolOptionSync so the toolbar's default state matches what the settings checkbox shows; the option key uses the allow- prefix so unset defaults to off. Takes effect on next session (setting is read at session init). The setting key (allow-multi-edge-toolbar-dock) is read by the existing local-options machinery and persists per-install without needing to be registered in libs/hbb_common's KEYS_LOCAL_SETTINGS. Can add that registration in a parallel hbb_common PR if preferred. * Fix remote toolbar drag positioning & persistence Align drag fraction calculation with the toolbar's actual travel range, keep preview sizing stable during drag, and preserve legacy horizontal position storage when multi-edge docking is disabled. Signed-off-by: fufesou * Remote toolbar snap edges 1. Translations 2. Apply option to remote windows on changed Signed-off-by: fufesou * fix: avoid remote toolbar docking jumps on setting reload Signed-off-by: fufesou * Fix remote toolbar docking updates and drag sync Signed-off-by: fufesou * refact: translation key Signed-off-by: fufesou * feat(toolbar-snap-edges): test web Signed-off-by: fufesou * Fix remote toolbar docking sync and vertical layout Signed-off-by: fufesou * Fix remote toolbar monitor controls on side docks Signed-off-by: fufesou --------- Signed-off-by: fufesou Co-authored-by: fufesou --- flutter/lib/consts.dart | 4 + .../desktop/pages/desktop_setting_page.dart | 10 + .../lib/desktop/widgets/remote_toolbar.dart | 841 +++++++++++++++--- flutter/lib/models/input_model.dart | 2 +- src/lang/ar.rs | 1 + src/lang/be.rs | 1 + src/lang/bg.rs | 1 + src/lang/ca.rs | 1 + src/lang/cn.rs | 1 + src/lang/cs.rs | 1 + src/lang/da.rs | 1 + src/lang/de.rs | 1 + src/lang/el.rs | 1 + src/lang/en.rs | 1 + src/lang/eo.rs | 1 + src/lang/es.rs | 1 + src/lang/et.rs | 1 + src/lang/eu.rs | 1 + src/lang/fa.rs | 1 + src/lang/fi.rs | 1 + src/lang/fr.rs | 1 + src/lang/ge.rs | 1 + src/lang/gu.rs | 2 + src/lang/he.rs | 1 + src/lang/hi.rs | 3 + src/lang/hr.rs | 1 + src/lang/hu.rs | 1 + src/lang/id.rs | 1 + src/lang/it.rs | 1 + src/lang/ja.rs | 1 + src/lang/ko.rs | 1 + src/lang/kz.rs | 1 + src/lang/lt.rs | 1 + src/lang/lv.rs | 1 + src/lang/ml.rs | 3 + src/lang/nb.rs | 1 + src/lang/nl.rs | 1 + src/lang/pl.rs | 1 + src/lang/pt_PT.rs | 1 + src/lang/ptbr.rs | 1 + src/lang/ro.rs | 3 +- src/lang/ru.rs | 1 + src/lang/sc.rs | 1 + src/lang/sk.rs | 1 + src/lang/sl.rs | 1 + src/lang/sq.rs | 1 + src/lang/sr.rs | 1 + src/lang/sv.rs | 1 + src/lang/ta.rs | 1 + src/lang/template.rs | 1 + src/lang/th.rs | 1 + src/lang/tr.rs | 1 + src/lang/tw.rs | 1 + src/lang/uk.rs | 1 + src/lang/vi.rs | 1 + 55 files changed, 802 insertions(+), 113 deletions(-) diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 832b96d24..adf7b1d45 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -142,6 +142,10 @@ const String kOptionSwapLeftRightMouse = "swap-left-right-mouse"; const String kOptionCodecPreference = "codec-preference"; const String kOptionRemoteMenubarDragLeft = "remote-menubar-drag-left"; const String kOptionRemoteMenubarDragRight = "remote-menubar-drag-right"; +const String kOptionRemoteMenubarEdge = "remote-menubar-edge"; +const String kOptionRemoteMenubarFraction = "remote-menubar-frac"; +const String kOptionAllowMultiEdgeToolbarDock = + "allow-multi-edge-toolbar-dock"; const String kOptionHideAbTagsPanel = "hideAbTagsPanel"; const String kOptionRemoteMenubarState = "remoteMenubarState"; const String kOptionPeerSorting = "peer-sorting"; diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 2841c1d27..d1d620014 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -488,6 +488,16 @@ class _GeneralState extends State<_General> { _OptionCheckBox(context, 'Confirm before closing multiple tabs', kOptionEnableConfirmClosingTabs, isServer: false), + if (!bind.isIncomingOnly()) + _OptionCheckBox( + context, + 'allow-remote-toolbar-docking-any-edge', + kOptionAllowMultiEdgeToolbarDock, + isServer: false, + update: (_) { + reloadAllWindows(); + }, + ), _OptionCheckBox(context, 'Adaptive bitrate', kOptionEnableAbr), if (!isWeb) wallpaper(), if (!isWeb && !bind.isIncomingOnly()) ...[ diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 645cbe1cb..44a2dc1c7 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -28,6 +28,220 @@ import './kb_layout_type_chooser.dart'; import 'package:flutter_hbb/utils/scale.dart'; import 'package:flutter_hbb/common/widgets/custom_scale_base.dart'; +enum _ToolbarEdge { top, right, bottom, left } + +_ToolbarEdge _parseToolbarEdge(String? s) { + switch (s) { + case 'right': + return _ToolbarEdge.right; + case 'bottom': + return _ToolbarEdge.bottom; + case 'left': + return _ToolbarEdge.left; + default: + return _ToolbarEdge.top; + } +} + +String _toolbarEdgeToString(_ToolbarEdge e) { + switch (e) { + case _ToolbarEdge.top: + return 'top'; + case _ToolbarEdge.right: + return 'right'; + case _ToolbarEdge.bottom: + return 'bottom'; + case _ToolbarEdge.left: + return 'left'; + } +} + +bool _isHorizontalEdge(_ToolbarEdge e) => + e == _ToolbarEdge.top || e == _ToolbarEdge.bottom; + +const _legacyRemoteMenubarDragX = 'remote-menubar-drag-x'; + +double _clampToolbarFraction(double fraction, double left, double right) { + if (fraction < left) fraction = left; + if (fraction > right) fraction = right; + return fraction; +} + +Size _toolbarSizeForEdge(_ToolbarEdge edge, Size? measured) { + final isHorizontal = _isHorizontalEdge(edge); + final fallback = isHorizontal ? const Size(360, 40) : const Size(40, 360); + final size = measured ?? fallback; + final long = size.longestSide; + final short = size.shortestSide; + return Size(isHorizontal ? long : short, isHorizontal ? short : long); +} + +Offset _toolbarOffsetForEdge({ + required _ToolbarEdge edge, + required double fraction, + required Size parentSize, + required Size toolbarSize, +}) { + final xTravel = parentSize.width - toolbarSize.width; + final yTravel = parentSize.height - toolbarSize.height; + switch (edge) { + case _ToolbarEdge.top: + return Offset(xTravel * fraction, 0); + case _ToolbarEdge.bottom: + return Offset(xTravel * fraction, yTravel); + case _ToolbarEdge.left: + return Offset(0, yTravel * fraction); + case _ToolbarEdge.right: + return Offset(xTravel, yTravel * fraction); + } +} + +double _fractionForAlignedDrag({ + required double cursor, + required double grabOffset, + required double parentExtent, + required double toolbarExtent, + required double left, + required double right, +}) { + final travelExtent = parentExtent - toolbarExtent; + if (travelExtent <= 0) { + return _clampToolbarFraction(0.5, left, right); + } + return _clampToolbarFraction( + (cursor - grabOffset) / travelExtent, left, right); +} + +({double left, double right}) _fractionBoundsForEdge( + _ToolbarEdge edge, + double left, + double right, +) { + return _isHorizontalEdge(edge) + ? (left: left, right: right) + : (left: 0, right: 1); +} + +String _toolbarRawFraction({ + required bool multiEdgeEnabled, + required _ToolbarEdge edge, + required String? savedFraction, + required String? legacyFraction, +}) { + if (!multiEdgeEnabled) { + return (legacyFraction != null && legacyFraction.isNotEmpty) + ? legacyFraction + : '0.5'; + } + if (savedFraction != null && savedFraction.isNotEmpty) { + return savedFraction; + } + if (edge == _ToolbarEdge.top && + legacyFraction != null && + legacyFraction.isNotEmpty) { + return legacyFraction; + } + return '0.5'; +} + +// Returns the alignment for the wrapper Align that positions the entire +// toolbar against the given edge at the given fraction along that edge. +// Alignment uses [-1, 1] coordinates (0 = center). +Alignment _alignmentForEdge(_ToolbarEdge edge, double fraction) { + final f = fraction * 2 - 1; + switch (edge) { + case _ToolbarEdge.top: + return Alignment(f, -1); + case _ToolbarEdge.bottom: + return Alignment(f, 1); + case _ToolbarEdge.left: + return Alignment(-1, f); + case _ToolbarEdge.right: + return Alignment(1, f); + } +} + +// The drag handle hangs off the side of the toolbar facing away from the +// docked edge, so the icons themselves sit flush against that edge. +BorderRadius _collapseHandleBorderRadius(_ToolbarEdge edge) { + const r = Radius.circular(5); + switch (edge) { + case _ToolbarEdge.top: + return const BorderRadius.vertical(bottom: r); + case _ToolbarEdge.bottom: + return const BorderRadius.vertical(top: r); + case _ToolbarEdge.left: + return const BorderRadius.horizontal(right: r); + case _ToolbarEdge.right: + return const BorderRadius.horizontal(left: r); + } +} + +int _monitorMenuQuarterTurns(_ToolbarEdge edge) { + switch (edge) { + case _ToolbarEdge.left: + return 1; + case _ToolbarEdge.right: + return 3; + case _ToolbarEdge.top: + case _ToolbarEdge.bottom: + return 0; + } +} + +IconData _toolbarCollapseIcon(_ToolbarEdge edge, bool isCollapsed) { + switch (edge) { + case _ToolbarEdge.top: + return isCollapsed ? Icons.expand_more : Icons.expand_less; + case _ToolbarEdge.bottom: + return isCollapsed ? Icons.expand_less : Icons.expand_more; + case _ToolbarEdge.left: + return isCollapsed ? Icons.chevron_right : Icons.chevron_left; + case _ToolbarEdge.right: + return isCollapsed ? Icons.chevron_left : Icons.chevron_right; + } +} + +class _ToolbarDockingOptions { + _ToolbarDockingOptions({ + required this.edge, + required this.fraction, + required this.multiEdgeEnabled, + }); + + _ToolbarEdge edge; + double fraction; + bool multiEdgeEnabled; +} + +final _toolbarDockingOptionsBySession = {}; + +String _toolbarDockingCacheKey(SessionID sessionId) => sessionId.toString(); + +_ToolbarDockingOptions? _cachedToolbarDockingOptions(SessionID sessionId) => + _toolbarDockingOptionsBySession[_toolbarDockingCacheKey(sessionId)]; + +void _cacheToolbarDockingOptions({ + required SessionID sessionId, + required _ToolbarEdge edge, + required double fraction, + required bool multiEdgeEnabled, +}) { + final key = _toolbarDockingCacheKey(sessionId); + final cached = _toolbarDockingOptionsBySession[key]; + if (cached == null) { + _toolbarDockingOptionsBySession[key] = _ToolbarDockingOptions( + edge: edge, + fraction: fraction, + multiEdgeEnabled: multiEdgeEnabled, + ); + return; + } + cached.edge = edge; + cached.fraction = fraction; + cached.multiEdgeEnabled = multiEdgeEnabled; +} + class ToolbarState { late RxBool _pin; @@ -250,8 +464,26 @@ class RemoteToolbar extends StatefulWidget { class _RemoteToolbarState extends State { late Debouncer _debouncerHide; bool _isCursorOverImage = false; - final _fractionX = 0.5.obs; + final _fraction = 0.5.obs; + final _edge = _ToolbarEdge.top.obs; final _dragging = false.obs; + // Live drag preview: where the toolbar would dock if the user dropped now. + final _previewEdge = Rxn<_ToolbarEdge>(); + final _previewFraction = Rxn(); + // Measured size of the live toolbar, so the preview ghost matches reality + // (collapsed handle vs expanded toolbar). Updated after every layout pass. + final _toolbarSize = Rxn(); + final _toolbarKey = GlobalKey(debugLabel: 'remote_toolbar_root'); + // When false (default), the toolbar stays on the top edge and the drag + // handle just slides it horizontally — preserving long-standing UX while + // still fixing the bug where dragging only moved the handle. When true, + // the user has opted into multi-edge docking with nearest-edge snap. + // Kept in sync after settings-triggered rebuilds. + final _multiEdgeEnabled = false.obs; + final _dockingOptionsInitialized = false.obs; + bool _pendingDockingOptionSync = false; + int _dockingOptionSyncSerial = 0; + int _dragEpoch = 0; int get windowId => stateGlobal.windowId; @@ -273,16 +505,144 @@ class _RemoteToolbarState extends State { void _minimize() async => await WindowController.fromWindowId(windowId).minimize(); + Future _syncDockingOptions({required bool force}) async { + final syncSerial = ++_dockingOptionSyncSerial; + if (_dragging.isTrue) { + _deferDockingOptionsSync(); + return; + } + final dragEpoch = _dragEpoch; + + // Use the canonical helper so the option's documented default semantics + // apply (allow-* prefix => default false). Keeping it raw-string would + // diverge from how _OptionCheckBox displays the same key. + final multiEdgeEnabled = + mainGetLocalBoolOptionSync(kOptionAllowMultiEdgeToolbarDock); + final cached = _cachedToolbarDockingOptions(widget.ffi.sessionId); + if (cached == null && pi.isSet.isFalse) { + return; + } + final hadDockingOptions = cached != null; + final wasMultiEdgeEnabled = + cached?.multiEdgeEnabled ?? _multiEdgeEnabled.value; + if (!force && + hadDockingOptions && + wasMultiEdgeEnabled == multiEdgeEnabled) { + _pendingDockingOptionSync = false; + return; + } + + final savedFraction = await bind.sessionGetOption( + sessionId: widget.ffi.sessionId, arg: kOptionRemoteMenubarFraction); + // Backward compat: legacy horizontal-only position. + final legacyFraction = await bind.sessionGetOption( + sessionId: widget.ffi.sessionId, arg: _legacyRemoteMenubarDragX); + if (!mounted || syncSerial != _dockingOptionSyncSerial) return; + + var nextEdge = _edge.value; + var savedFractionForNextEdge = savedFraction; + var keepCurrentPosition = false; + if (!multiEdgeEnabled) { + nextEdge = _ToolbarEdge.top; + } else if (force || wasMultiEdgeEnabled || cached == null) { + final edgeStr = await bind.sessionGetOption( + sessionId: widget.ffi.sessionId, arg: kOptionRemoteMenubarEdge); + if (!mounted || syncSerial != _dockingOptionSyncSerial) return; + nextEdge = _parseToolbarEdge(edgeStr); + } else { + // The setting changed from top-only to multi-edge while this toolbar is + // already visible. Keep its current position instead of jumping to the + // last saved multi-edge dock. + nextEdge = cached.edge; + savedFractionForNextEdge = cached.fraction.toString(); + keepCurrentPosition = true; + } + + final rawFraction = _toolbarRawFraction( + multiEdgeEnabled: multiEdgeEnabled, + edge: nextEdge, + savedFraction: savedFractionForNextEdge, + legacyFraction: legacyFraction, + ); + // Clamp to the saved drag-bound contract so a corrupted or out-of-range + // saved value can't bypass it until the user drags again. + final dragLeft = double.tryParse( + bind.mainGetLocalOption(key: kOptionRemoteMenubarDragLeft)) ?? + 0.0; + final dragRight = double.tryParse( + bind.mainGetLocalOption(key: kOptionRemoteMenubarDragRight)) ?? + 1.0; + final fractionBounds = + _fractionBoundsForEdge(nextEdge, dragLeft, dragRight); + final nextFraction = (double.tryParse(rawFraction) ?? 0.5) + .clamp(fractionBounds.left, fractionBounds.right) + .toDouble(); + if (!mounted || syncSerial != _dockingOptionSyncSerial) return; + if (_dragging.isTrue || dragEpoch != _dragEpoch) { + _deferDockingOptionsSync(); + return; + } + _edge.value = nextEdge; + _fraction.value = nextFraction; + _multiEdgeEnabled.value = multiEdgeEnabled; + _dockingOptionsInitialized.value = true; + _cacheToolbarDockingOptions( + sessionId: widget.ffi.sessionId, + edge: nextEdge, + fraction: nextFraction, + multiEdgeEnabled: multiEdgeEnabled, + ); + _pendingDockingOptionSync = false; + if (!multiEdgeEnabled || keepCurrentPosition) { + bind.sessionPeerOption( + sessionId: widget.ffi.sessionId, + name: kOptionRemoteMenubarEdge, + value: _toolbarEdgeToString(nextEdge), + ); + bind.sessionPeerOption( + sessionId: widget.ffi.sessionId, + name: kOptionRemoteMenubarFraction, + value: nextFraction.toString(), + ); + } + } + + void _deferDockingOptionsSync() { + _pendingDockingOptionSync = true; + if (_dragging.isFalse) { + _syncDockingOptionsAfterDragIfNeeded(); + } + } + + void _markToolbarDragEpoch() { + ++_dragEpoch; + } + + void _syncDockingOptionsAfterDragIfNeeded() { + if (!_pendingDockingOptionSync) return; + WidgetsBinding.instance.addPostFrameCallback((_) async { + await _syncDockingOptions(force: false); + }); + } + @override initState() { super.initState(); + final cached = _cachedToolbarDockingOptions(widget.ffi.sessionId); + final multiEdgeEnabled = + mainGetLocalBoolOptionSync(kOptionAllowMultiEdgeToolbarDock); + final shouldResetToTop = + cached != null && cached.multiEdgeEnabled && !multiEdgeEnabled; + if (cached != null && !shouldResetToTop) { + _edge.value = cached.edge; + _fraction.value = cached.fraction; + _multiEdgeEnabled.value = multiEdgeEnabled; + _dockingOptionsInitialized.value = true; + } + WidgetsBinding.instance.addPostFrameCallback((_) async { - _fractionX.value = double.tryParse(await bind.sessionGetOption( - sessionId: widget.ffi.sessionId, - arg: 'remote-menubar-drag-x') ?? - '0.5') ?? - 0.5; + await _syncDockingOptions(force: cached == null || shouldResetToTop); // Initialize toolbar states (collapse, hide) from session options widget.state.init(widget.ffi.sessionId); }); @@ -303,6 +663,14 @@ class _RemoteToolbarState extends State { }); } + @override + void didUpdateWidget(covariant RemoteToolbar oldWidget) { + super.didUpdateWidget(oldWidget); + WidgetsBinding.instance.addPostFrameCallback((_) async { + await _syncDockingOptions(force: false); + }); + } + _debouncerHideProc(int v) { if (!pin && collapse.isFalse && _isCursorOverImage && _dragging.isFalse) { collapse.value = true; @@ -311,64 +679,130 @@ class _RemoteToolbarState extends State { @override dispose() { - super.dispose(); - + ++_dockingOptionSyncSerial; widget.onEnterOrLeaveImageCleaner(identityHashCode(this)); + super.dispose(); } @override Widget build(BuildContext context) { return Obx(() { // Wait for initialization to complete to prevent flickering - if (!widget.state.initialized.value) { + if (!widget.state.initialized.value || + !_dockingOptionsInitialized.value) { return const SizedBox.shrink(); } // If toolbar is hidden, return empty widget if (hide.value) { return const SizedBox.shrink(); } - return Align( - alignment: Alignment.topCenter, - child: collapse.isFalse - ? _buildToolbar(context) - : _buildDraggableCollapse(context), + final edge = _edge.value; + final isHorizontal = _isHorizontalEdge(edge); + + // Measure the live toolbar after every layout so the preview ghost can + // match its actual footprint (collapsed handle vs expanded toolbar). + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_dragging.isTrue) return; + final ro = _toolbarKey.currentContext?.findRenderObject(); + if (ro is RenderBox && ro.hasSize) { + final s = ro.size; + if (_toolbarSize.value != s) _toolbarSize.value = s; + } + }); + + final toolbar = Align( + alignment: _alignmentForEdge(edge, _fraction.value), + child: KeyedSubtree( + key: _toolbarKey, + child: collapse.isFalse + ? _buildToolbar(context, edge, isHorizontal) + : _buildDraggableCollapse(context, edge, isHorizontal), + ), + ); + + // Always return the Stack — even when not dragging — so the toolbar's + // position in the Element tree stays stable. Wrapping/unwrapping it + // mid-drag was killing the Draggable's gesture state. + return Stack( + fit: StackFit.expand, + children: [ + IgnorePointer( + child: Obx(() { + final pe = _previewEdge.value; + final pf = _previewFraction.value; + if (!_dragging.isTrue || pe == null || pf == null) { + return const SizedBox.shrink(); + } + return _buildDragPreview(context, pe, pf, _toolbarSize.value); + }), + ), + toolbar, + ], ); }); } - Widget _buildDraggableCollapse(BuildContext context) { + Widget _buildDragPreview(BuildContext context, _ToolbarEdge edge, + double fraction, Size? measured) { + final color = Theme.of(context).colorScheme.primary; + // Use the measured live toolbar size so collapsed vs expanded looks + // right. The current orientation may differ from the preview orientation + // (e.g. dragging a top-docked toolbar toward the left edge), so swap the + // long/short axes when previewing a different orientation. + final previewSize = _toolbarSizeForEdge(edge, measured); + return Align( + alignment: _alignmentForEdge(edge, fraction), + child: Container( + width: previewSize.width, + height: previewSize.height, + decoration: BoxDecoration( + color: color.withOpacity(0.10), + borderRadius: BorderRadius.circular(6), + border: Border.all(color: color.withOpacity(0.55), width: 1.5), + ), + ), + ); + } + + Widget _buildDraggableCollapse( + BuildContext context, _ToolbarEdge edge, bool isHorizontal) { return Obx(() { if (collapse.isFalse && _dragging.isFalse) { triggerAutoHide(); } - final borderRadius = BorderRadius.vertical( - bottom: Radius.circular(5), - ); - return Align( - alignment: FractionalOffset(_fractionX.value, 0), - child: Offstage( - offstage: _dragging.isTrue, - child: Material( - elevation: _ToolbarTheme.elevation, - shadowColor: MyTheme.color(context).shadow, + final borderRadius = _collapseHandleBorderRadius(edge); + return Offstage( + offstage: _dragging.isTrue, + child: Material( + elevation: _ToolbarTheme.elevation, + shadowColor: MyTheme.color(context).shadow, + borderRadius: borderRadius, + child: _DraggableShowHide( + id: widget.id, + sessionId: widget.ffi.sessionId, + dragging: _dragging, + fraction: _fraction, + edge: _edge, + previewEdge: _previewEdge, + previewFraction: _previewFraction, + toolbarSize: _toolbarSize, + markDragEpoch: _markToolbarDragEpoch, + syncDockingOptionsAfterDragIfNeeded: + _syncDockingOptionsAfterDragIfNeeded, + isHorizontal: isHorizontal, + multiEdgeEnabled: _multiEdgeEnabled.value, + toolbarState: widget.state, + setFullscreen: _setFullscreen, + setMinimize: _minimize, borderRadius: borderRadius, - child: _DraggableShowHide( - id: widget.id, - sessionId: widget.ffi.sessionId, - dragging: _dragging, - fractionX: _fractionX, - toolbarState: widget.state, - setFullscreen: _setFullscreen, - setMinimize: _minimize, - borderRadius: borderRadius, - ), ), ), ); }); } - Widget _buildToolbar(BuildContext context) { + Widget _buildToolbar( + BuildContext context, _ToolbarEdge edge, bool isHorizontal) { final List toolbarItems = []; toolbarItems.add(_PinMenu(state: widget.state)); if (!isWebDesktop) { @@ -382,6 +816,7 @@ class _RemoteToolbarState extends State { return _MonitorMenu( id: widget.id, ffi: widget.ffi, + edge: edge, setRemoteState: widget.setRemoteState); } else { return Offstage(); @@ -407,37 +842,53 @@ class _RemoteToolbarState extends State { if (!isWeb) toolbarItems.add(_RecordMenu()); toolbarItems.add(_CloseMenu(id: widget.id, ffi: widget.ffi)); final toolbarBorderRadius = BorderRadius.all(Radius.circular(4.0)); - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Material( - elevation: _ToolbarTheme.elevation, - shadowColor: MyTheme.color(context).shadow, - borderRadius: toolbarBorderRadius, - color: Theme.of(context) - .menuBarTheme - .style - ?.backgroundColor - ?.resolve(MaterialState.values.toSet()), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Theme( - data: themeData(), - child: _ToolbarTheme.borderWrapper( - context, - Row( - children: [ - SizedBox(width: _ToolbarTheme.buttonHMargin * 2), - ...toolbarItems, - SizedBox(width: _ToolbarTheme.buttonHMargin * 2) - ], - ), - toolbarBorderRadius), - ), - ), + // innerAxis: how the toolbar icons themselves flow. + // outerAxis: how the toolbar block and the handle stack against each other + // (perpendicular to the dock edge, so the handle hangs off the interior face). + final innerAxis = isHorizontal ? Axis.horizontal : Axis.vertical; + final outerAxis = isHorizontal ? Axis.vertical : Axis.horizontal; + final spacer = isHorizontal + ? SizedBox(width: _ToolbarTheme.buttonHMargin * 2) + : SizedBox(height: _ToolbarTheme.buttonHMargin * 2); + final toolbarMaterial = Material( + elevation: _ToolbarTheme.elevation, + shadowColor: MyTheme.color(context).shadow, + borderRadius: toolbarBorderRadius, + color: Theme.of(context) + .menuBarTheme + .style + ?.backgroundColor + ?.resolve(MaterialState.values.toSet()), + child: SingleChildScrollView( + scrollDirection: innerAxis, + child: Theme( + data: themeData(), + child: _ToolbarTheme.borderWrapper( + context, + Flex( + direction: innerAxis, + mainAxisSize: MainAxisSize.min, + children: [ + spacer, + ...toolbarItems, + spacer, + ], + ), + toolbarBorderRadius), ), - _buildDraggableCollapse(context), - ], + ), + ); + final handle = _buildDraggableCollapse(context, edge, isHorizontal); + // The handle hangs off the interior face of the toolbar (away from the + // docked edge), centered along that face by the Flex's default cross-axis + // alignment, so the icons themselves sit flush against the docked edge. + final children = (edge == _ToolbarEdge.top || edge == _ToolbarEdge.left) + ? [toolbarMaterial, handle] + : [handle, toolbarMaterial]; + return Flex( + direction: outerAxis, + mainAxisSize: MainAxisSize.min, + children: children, ); } @@ -516,11 +967,13 @@ class _MobileActionMenu extends StatelessWidget { class _MonitorMenu extends StatelessWidget { final String id; final FFI ffi; + final _ToolbarEdge edge; final Function(VoidCallback) setRemoteState; const _MonitorMenu({ Key? key, required this.id, required this.ffi, + required this.edge, required this.setRemoteState, }) : super(key: key); @@ -531,9 +984,17 @@ class _MonitorMenu extends StatelessWidget { !isWeb && ffi.ffiModel.pi.isSupportMultiDisplay; @override - Widget build(BuildContext context) => showMonitorsToolbar - ? buildMultiMonitorMenu(context) - : Obx(() => buildMonitorMenu(context)); + Widget build(BuildContext context) { + final child = showMonitorsToolbar + ? buildMultiMonitorMenu(context) + : Obx(() => buildMonitorMenu(context)); + final quarterTurns = _monitorMenuQuarterTurns(edge); + if (quarterTurns == 0) return child; + return RotatedBox( + quarterTurns: quarterTurns, + child: child, + ); + } Widget buildMonitorMenu(BuildContext context) { final width = SimpleWrapper(0); @@ -665,7 +1126,8 @@ class _MonitorMenu extends StatelessWidget { } final scale = _ToolbarTheme.buttonSize / rect.height * 0.75; - final startY = (_ToolbarTheme.buttonSize - rect.height * scale) * 0.5; + final height = rect.height * scale; + final startY = (_ToolbarTheme.buttonSize - height) * 0.5; final startX = startY; final children = []; @@ -708,7 +1170,7 @@ class _MonitorMenu extends StatelessWidget { width.value = rect.width * scale + startX * 2; return SizedBox( width: width.value, - height: rect.height * scale + startY * 2, + height: height + startY * 2, child: Stack( children: children, ), @@ -2519,7 +2981,18 @@ class RdoMenuButton extends StatelessWidget { class _DraggableShowHide extends StatefulWidget { final String id; final SessionID sessionId; - final RxDouble fractionX; + final RxDouble fraction; + final Rx<_ToolbarEdge> edge; + final Rxn<_ToolbarEdge> previewEdge; + final Rxn previewFraction; + final Rxn toolbarSize; + final VoidCallback markDragEpoch; + final VoidCallback syncDockingOptionsAfterDragIfNeeded; + final bool isHorizontal; + // Whether multi-edge docking is enabled for this session (toggled in + // Settings -> Other). When false, the drag handle slides the toolbar + // horizontally on the top edge and never switches edges. + final bool multiEdgeEnabled; final RxBool dragging; final ToolbarState toolbarState; final BorderRadius borderRadius; @@ -2531,7 +3004,15 @@ class _DraggableShowHide extends StatefulWidget { Key? key, required this.id, required this.sessionId, - required this.fractionX, + required this.fraction, + required this.edge, + required this.previewEdge, + required this.previewFraction, + required this.toolbarSize, + required this.markDragEpoch, + required this.syncDockingOptionsAfterDragIfNeeded, + required this.isHorizontal, + required this.multiEdgeEnabled, required this.dragging, required this.toolbarState, required this.setFullscreen, @@ -2544,10 +3025,12 @@ class _DraggableShowHide extends StatefulWidget { } class _DraggableShowHideState extends State<_DraggableShowHide> { - Offset position = Offset.zero; - Size size = Size.zero; double left = 0.0; double right = 1.0; + Offset? _lastPointerDown; + Offset? _dragGrabOffset; + double? _dragLongAxisGrabOffset; + Size? _dragToolbarSize; RxBool get collapse => widget.toolbarState.collapse; @@ -2573,41 +3056,174 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { } } + // Bias applied to the currently-previewed edge so a drag hovering between + // two edges doesn't flicker. Only relevant when multi-edge is enabled. + static const double _switchHysteresisPx = 50.0; + + _ToolbarEdge _nearestToolbarEdge(Offset cursor, Size mediaSize) { + if (!widget.multiEdgeEnabled) return widget.edge.value; + + double rawDist(_ToolbarEdge e) { + switch (e) { + case _ToolbarEdge.top: + return cursor.dy; + case _ToolbarEdge.bottom: + return mediaSize.height - cursor.dy; + case _ToolbarEdge.left: + return cursor.dx; + case _ToolbarEdge.right: + return mediaSize.width - cursor.dx; + } + } + + final previewed = widget.previewEdge.value; + var winner = widget.edge.value; + var best = double.infinity; + for (final e in _ToolbarEdge.values) { + final biased = + e == previewed ? rawDist(e) - _switchHysteresisPx : rawDist(e); + if (biased < best) { + best = biased; + winner = e; + } + } + return winner; + } + + void _ensureDragGrabOffset(Offset cursor) { + if (_dragGrabOffset != null) return; + final mediaSize = MediaQueryData.fromView(View.of(context)).size; + final toolbarSize = + _toolbarSizeForEdge(widget.edge.value, widget.toolbarSize.value); + _dragToolbarSize = toolbarSize; + final toolbarOffset = _toolbarOffsetForEdge( + edge: widget.edge.value, + fraction: widget.fraction.value, + parentSize: mediaSize, + toolbarSize: toolbarSize, + ); + _dragGrabOffset = cursor - toolbarOffset; + _dragLongAxisGrabOffset = _isHorizontalEdge(widget.edge.value) + ? _dragGrabOffset?.dx + : _dragGrabOffset?.dy; + } + + double _dragGrabOffsetForEdge(_ToolbarEdge edge, Size toolbarSize) { + final offset = _dragLongAxisGrabOffset ?? 0; + final extent = + _isHorizontalEdge(edge) ? toolbarSize.width : toolbarSize.height; + return _clampToolbarFraction(offset, 0, extent); + } + + void _updatePreview(Offset cursor) { + _ensureDragGrabOffset(cursor); + final mediaSize = MediaQueryData.fromView(View.of(context)).size; + final winner = _nearestToolbarEdge(cursor, mediaSize); + widget.previewEdge.value = winner; + + final toolbarSize = _toolbarSizeForEdge(winner, _dragToolbarSize); + final grabOffset = _dragGrabOffsetForEdge(winner, toolbarSize); + final double frac; + if (winner == _ToolbarEdge.top || winner == _ToolbarEdge.bottom) { + frac = _fractionForAlignedDrag( + cursor: cursor.dx, + grabOffset: grabOffset, + parentExtent: mediaSize.width, + toolbarExtent: toolbarSize.width, + left: left, + right: right, + ); + } else { + final fractionBounds = _fractionBoundsForEdge(winner, left, right); + frac = _fractionForAlignedDrag( + cursor: cursor.dy, + grabOffset: grabOffset, + parentExtent: mediaSize.height, + toolbarExtent: toolbarSize.height, + left: fractionBounds.left, + right: fractionBounds.right, + ); + } + widget.previewFraction.value = frac; + } + + void _resetDragTracking() { + _lastPointerDown = null; + _dragGrabOffset = null; + _dragLongAxisGrabOffset = null; + _dragToolbarSize = null; + } + + void _commitPreview() { + final newEdge = widget.previewEdge.value; + final frac = widget.previewFraction.value; + widget.previewEdge.value = null; + widget.previewFraction.value = null; + widget.dragging.value = false; + widget.markDragEpoch(); + _resetDragTracking(); + widget.syncDockingOptionsAfterDragIfNeeded(); + if (newEdge == null || frac == null) return; + widget.edge.value = newEdge; + widget.fraction.value = frac; + _cacheToolbarDockingOptions( + sessionId: widget.sessionId, + edge: newEdge, + fraction: frac, + multiEdgeEnabled: widget.multiEdgeEnabled, + ); + bind.sessionPeerOption( + sessionId: widget.sessionId, + name: kOptionRemoteMenubarEdge, + value: _toolbarEdgeToString(newEdge), + ); + bind.sessionPeerOption( + sessionId: widget.sessionId, + name: kOptionRemoteMenubarFraction, + value: frac.toString(), + ); + if (widget.multiEdgeEnabled) { + return; + } + bind.sessionPeerOption( + sessionId: widget.sessionId, + name: _legacyRemoteMenubarDragX, + value: frac.toString(), + ); + } + Widget _buildDraggable(BuildContext context) { - return Draggable( - axis: Axis.horizontal, - child: Icon( - Icons.drag_indicator, - size: 20, - color: MyTheme.color(context).drag_indicator, + return Listener( + onPointerDown: (event) => _lastPointerDown = event.position, + child: Draggable( + // When multi-edge docking is off the toolbar stays on the top edge, + // so lock the feedback to horizontal motion — otherwise the handle + // floats away from the top while dragging and the toolbar looks + // unmoored. When multi-edge is on we need 2D drag for snap-to-edge. + axis: widget.multiEdgeEnabled ? null : Axis.horizontal, + child: Icon( + widget.isHorizontal ? Icons.drag_indicator : Icons.drag_handle, + size: 20, + color: MyTheme.color(context).drag_indicator, + ), + feedback: widget, + onDragStarted: () { + widget.markDragEpoch(); + final pointerDown = _lastPointerDown; + if (pointerDown != null) { + _ensureDragGrabOffset(pointerDown); + } + widget.dragging.value = true; + // Seed the preview at the current docked edge/fraction so something + // shows the instant the drag begins, before the first onDragUpdate. + widget.previewEdge.value = widget.edge.value; + widget.previewFraction.value = widget.fraction.value; + }, + onDragUpdate: (details) { + _updatePreview(details.globalPosition); + }, + onDragEnd: (_) => _commitPreview(), ), - feedback: widget, - onDragStarted: (() { - final RenderObject? renderObj = context.findRenderObject(); - if (renderObj != null) { - final RenderBox renderBox = renderObj as RenderBox; - size = renderBox.size; - position = renderBox.localToGlobal(Offset.zero); - } - widget.dragging.value = true; - }), - onDragEnd: (details) { - final mediaSize = MediaQueryData.fromView(View.of(context)).size; - widget.fractionX.value += - (details.offset.dx - position.dx) / (mediaSize.width - size.width); - if (widget.fractionX.value < left) { - widget.fractionX.value = left; - } - if (widget.fractionX.value > right) { - widget.fractionX.value = right; - } - bind.sessionPeerOption( - sessionId: widget.sessionId, - name: 'remote-menubar-drag-x', - value: widget.fractionX.value.toString(), - ); - widget.dragging.value = false; - }, ); } @@ -2637,7 +3253,9 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { ); } - final child = Row( + final axis = widget.isHorizontal ? Axis.horizontal : Axis.vertical; + final child = Flex( + direction: axis, mainAxisSize: MainAxisSize.min, children: [ _buildDraggable(context), @@ -2678,7 +3296,7 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { message: translate( collapse.isFalse ? 'Hide Toolbar' : 'Show Toolbar'), child: Icon( - collapse.isFalse ? Icons.expand_less : Icons.expand_more, + _toolbarCollapseIcon(widget.edge.value, collapse.isTrue), size: iconSize, ), ))), @@ -2720,7 +3338,8 @@ class _DraggableShowHideState extends State<_DraggableShowHide> { borderRadius: widget.borderRadius, ), child: SizedBox( - height: 20, + height: widget.isHorizontal ? 20 : null, + width: widget.isHorizontal ? null : 20, child: child, ), ), diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index 6fdffd796..984d6a25c 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -346,7 +346,7 @@ class InputModel { /// which runs per-engine, so each isolate registers its own handler tied /// to its own set of InputModels. static void initSideButtonChannel() { - if (!Platform.isLinux) return; + if (!isLinux) return; if (_sideButtonChannelInitialized) return; _sideButtonChannelInitialized = true; diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 4113c1391..e13404802 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "كلمة المرور مخفية"), ("preset-password-in-use-tip", "كلمة المرور المحددة مسبقًا قيد الاستخدام"), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index 1a3260c5a..9f6b69c8b 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "Зададзены пастаянны пароль (скрыты)."), ("preset-password-in-use-tip", "Пададзены пароль цяпер выкарыстоўваецца"), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 17a89ce07..0aa61b1eb 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 799ca951f..2f706cc89 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 1ff10c49d..a90e5e194 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "永久密码已设置(已隐藏)"), ("preset-password-in-use-tip", "当前使用预设密码"), ("Enable privacy mode", "允许隐私模式"), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 2b9c6219e..7f50d826f 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 7410124df..c9d3b4eb0 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index 030bc626d..e6233e91e 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "Ein permanentes Passwort wurde festgelegt (ausgeblendet)."), ("preset-password-in-use-tip", "Das voreingestellte Passwort wird derzeit verwendet."), ("Enable privacy mode", "Datenschutzmodus aktivieren"), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 0633889a7..d03bb069c 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 73974a2e5..595169b8a 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -274,5 +274,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("keep-awake-during-incoming-sessions-label", "Keep screen awake during incoming sessions"), ("password-hidden-tip", "Permanent password is set (hidden)."), ("preset-password-in-use-tip", "Preset password is currently in use."), + ("allow-remote-toolbar-docking-any-edge", "Allow docking remote toolbar to any window edge"), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 16d43c9b4..131a85fbf 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index b822432a0..5e73b58a8 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "La contraseña permanente está ajustada a (oculta)."), ("preset-password-in-use-tip", "Se está usando la contraseña predeterminada."), ("Enable privacy mode", "Habilitar modo privado"), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index a00c312b8..76abc8563 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index aaf8a8be8..9e19d1fea 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index d34e4239e..9e01b7eb0 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fi.rs b/src/lang/fi.rs index 1bddd39d1..f8283685b 100644 --- a/src/lang/fi.rs +++ b/src/lang/fi.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 6f7bb2880..f21d9b0df 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "Le mot de passe permanent est défini (masqué)."), ("preset-password-in-use-tip", "Le mot de passe prédéfini est actuellement utilisé."), ("Enable privacy mode", "Activer le mode de confidentialité"), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ge.rs b/src/lang/ge.rs index fba2fd83d..2fc8f282d 100644 --- a/src/lang/ge.rs +++ b/src/lang/ge.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/gu.rs b/src/lang/gu.rs index 8b8568c85..ac0a588a8 100644 --- a/src/lang/gu.rs +++ b/src/lang/gu.rs @@ -654,6 +654,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accessible devices", "એક્સેસિબલ ઉપકરણો"), ("upgrade_remote_rustdesk_client_to_{}_tip", "રિમોટ ક્લાયન્ટને {} માં અપગ્રેડ કરો"), ("d3d_render_tip", "D3D રેન્ડરિંગ વાપરો"), + ("Use D3D rendering", ""), ("Printer", "પ્રિન્ટર"), ("printer-os-requirement-tip", "પ્રિન્ટિંગ માટે Windows જરૂરી છે."), ("printer-requires-installed-{}-client-tip", "આ માટે {} ક્લાયન્ટ ઇન્સ્ટોલ હોવું જોઈએ."), @@ -743,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "સુરક્ષા માટે પાસવર્ડ છુપાવેલ છે."), ("preset-password-in-use-tip", "પ્રીસેટ પાસવર્ડ વપરાશમાં છે."), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/he.rs b/src/lang/he.rs index 682ee0c46..44b940784 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hi.rs b/src/lang/hi.rs index d35095fd1..904d43118 100644 --- a/src/lang/hi.rs +++ b/src/lang/hi.rs @@ -654,6 +654,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accessible devices", "सुलभ डिवाइस"), ("upgrade_remote_rustdesk_client_to_{}_tip", "रिमोट RustDesk क्लाइंट को संस्करण {} में अपग्रेड करें"), ("d3d_render_tip", "D3D रेंडरिंग का उपयोग करें"), + ("Use D3D rendering", ""), ("Printer", "प्रिंटर"), ("printer-os-requirement-tip", "प्रिंटिंग के लिए Windows आवश्यक है।"), ("printer-requires-installed-{}-client-tip", "इसके लिए क्लाइंट साइड पर {} इंस्टॉल होना चाहिए।"), @@ -742,5 +743,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "प्रदर्शित नाम"), ("password-hidden-tip", "पासवर्ड सुरक्षा के लिए छिपा हुआ है।"), ("preset-password-in-use-tip", "पूर्व-निर्धारित पासवर्ड उपयोग में है।"), + ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index 505b01df9..0593ff6b7 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index b4cbc1f23..3eb16890f 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "Állandó jelszó lett beállítva (rejtett)."), ("preset-password-in-use-tip", "Jelenleg az alapértelmezett jelszót használja."), ("Enable privacy mode", "Adatvédelmi mód aktiválása"), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index bbd95e79a..bcda0a3a8 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 479551fcc..a5132e027 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "È impostata una password permanente (nascosta)."), ("preset-password-in-use-tip", "È attualmente in uso la password preimpostata."), ("Enable privacy mode", "Abilita modalità privacy"), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index b55a6664f..2879e86bf 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "永続的なパスワードが設定されています (非表示)"), ("preset-password-in-use-tip", "プリセットパスワードが現在使用されています"), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index de68574e1..350d570b0 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "영구 비밀번호가 설정되었습니다 (숨김)."), ("preset-password-in-use-tip", "현재 사전 설정된 비밀번호가 사용 중입니다."), ("Enable privacy mode", "개인정보 보호 모드 사용함"), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index a2a1624f7..4476fadc7 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 82422c30a..47ace51ae 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 906d056bd..4f8e1f59f 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ml.rs b/src/lang/ml.rs index 099f1d385..4dcfe9e74 100644 --- a/src/lang/ml.rs +++ b/src/lang/ml.rs @@ -654,6 +654,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accessible devices", "ലഭ്യമായ ഉപകരണങ്ങൾ"), ("upgrade_remote_rustdesk_client_to_{}_tip", "റിമോട്ട് പതിപ്പ് {} ലേക്ക് മാറ്റുക"), ("d3d_render_tip", "D3D റെൻഡറിംഗ് ഉപയോഗിക്കുക"), + ("Use D3D rendering", ""), ("Printer", "പ്രിന്റർ"), ("printer-os-requirement-tip", "പ്രിന്റിംഗിന് വിൻഡോസ് വേണം."), ("printer-requires-installed-{}-client-tip", "ഇതിന് {} ക്ലയന്റ് ഇൻസ്റ്റാൾ ചെയ്യണം."), @@ -742,5 +743,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Display Name", "ഡിസ്‌പ്ലേ പേര്"), ("password-hidden-tip", "സുരക്ഷയ്ക്കായി പാസ്‌വേഡ് മറച്ചിരിക്കുന്നു."), ("preset-password-in-use-tip", "പ്രീസെറ്റ് പാസ്‌വേഡ് ഉപയോഗത്തിലാണ്."), + ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index 5795b9eeb..9325dfa1f 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 0f91d6a61..55d272666 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "Er is een permanent wachtwoord ingesteld (verborgen)."), ("preset-password-in-use-tip", "Het basis wachtwoord is momenteel in gebruik."), ("Enable privacy mode", "Privacymodus inschakelen"), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 972afc170..fdf4ae8c5 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "Ustawiono (ukryto) stare hasło."), ("preset-password-in-use-tip", "Obecnie używane jest hasło domyślne."), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 899c8da71..4138b46e4 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 36581d4f1..1428a71d0 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "A senha permanente está definida como (oculta)."), ("preset-password-in-use-tip", "A senha predefinida está sendo usada."), ("Enable privacy mode", "Habilitar modo de privacidade"), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 45b22684e..bde4a4201 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -540,7 +540,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("auto_disconnect_option_tip", "Deconectează automat sesiunile de la distanță după o perioadă de inactivitate."), ("Connection failed due to inactivity", "Conexiunea a eșuat din cauza inactivității"), ("Check for software update on startup", "Verifică actualizări la pornire"), - ("upgrade_rustdesk_server_pro_{}_tip", "Versiunea serverului RustDesk Pro este mai mică decât {}. Te rugăm să o actualizezi."), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Versiunea serverului RustDesk Pro este mai mică decât {}. Te rugăm să o actualizezi."), ("pull_group_failed_tip", "Sincronizarea grupului a eșuat. Verifică conexiunea la rețea sau autentifică-te din nou."), ("Filter by intersection", "Filtrează prin intersecție"), ("Remove wallpaper during incoming sessions", "Elimină imaginea de fundal în timpul sesiunilor primite"), @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "Parola este ascunsă din motive de securitate. Fă clic pe pictograma ochiului pentru a o afișa."), ("preset-password-in-use-tip", "Se folosește o parolă prestabilită. Se recomandă setarea unei parole personalizate pentru securitate sporită."), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 3917c6fa2..2605582f4 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "Установлен постоянный пароль (скрытый)."), ("preset-password-in-use-tip", "Установленный пароль сейчас используется."), ("Enable privacy mode", "Использовать режим конфиденциальности"), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs index 68ce541f2..06919b752 100644 --- a/src/lang/sc.rs +++ b/src/lang/sc.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 6b4e16688..963f48728 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index 3f35dea88..0f85af0c3 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index f7f6c16d4..7c965cd45 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index bedbe4856..fc33e4671 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index eda7851c1..664dc4745 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs index 6e5652560..93aeb6462 100644 --- a/src/lang/ta.rs +++ b/src/lang/ta.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 5e25801d2..33b359c5e 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index c2d058c98..a24c60bf6 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index d93ad4f68..c28086cc9 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "Parola gizli"), ("preset-password-in-use-tip", "Önceden ayarlanmış parola kullanılıyor"), ("Enable privacy mode", "Gizlilik modunu etkinleştir"), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index b23b84949..6df025303 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", "固定密碼已設定(已隱藏)"), ("preset-password-in-use-tip", "目前正在使用預設密碼"), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 3e1c4f25e..7107bc261 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vi.rs b/src/lang/vi.rs index 3fadb0efc..0910025ed 100644 --- a/src/lang/vi.rs +++ b/src/lang/vi.rs @@ -744,5 +744,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("password-hidden-tip", ""), ("preset-password-in-use-tip", ""), ("Enable privacy mode", ""), + ("allow-remote-toolbar-docking-any-edge", ""), ].iter().cloned().collect(); }