Compare commits

..

1 Commits

Author SHA1 Message Date
rustdesk
04faf21c78 feat: keyboard shortcuts in remote sessions
Add an opt-in keyboard-shortcut system that triggers session
actions (Send Ctrl+Alt+Del, Toggle Fullscreen, Switch Display,
Screenshot, Switch Tab, etc.) via three-modifier combinations
during a remote session.

Architecture
- Native: src/keyboard/shortcuts.rs intercepts at the encoder
  layer (process_event and process_event_with_session), so the
  feature is input-source-independent. Bindings persist as a
  single JSON blob in LocalConfig.
- Web: matching + keydown intercept live in the separate hand-
  written TS client at flutter/web/js/ (gitignored, not in this
  repo). flutter/lib/web/bridge.dart::mainInit registers
  window.onShortcutTriggered so the JS matcher can dispatch
  back into the active session's ShortcutModel; the bridge's
  mainReloadKeyboardShortcuts forwards to a JS reloadShortcuts
  on settings writes.
- Three-modifier prefix (Ctrl+Alt+Shift; Cmd+Option+Shift on
  macOS/iOS) sidesteps the need for a pass-through toggle.
- Flutter native path threads the explicit per-call SessionID
  for tab-precise routing; rdev path uses globally-current
  session.

UI
- Settings -> General -> Keyboard Shortcuts opens a dedicated
  configuration page; desktop and mobile share a body widget.
- Recording dialog with live capture, prefix validation, and a
  conflict-replace flow.
- Toolbar menu items display the bound shortcut inline.
- Default bindings (adapted from AnyDesk):
    +Del    Send Ctrl+Alt+Del
    +Enter  Toggle Fullscreen
    +Left/Right  Switch Display Prev/Next
    +P      Screenshot
    +1..9   Switch Session Tab

Other
- AGENTS.md: documented (a) flutter_rust_bridge_codegen needs
  a pinned version + Dart bridge wrappers should be hand-
  written, and (b) the Web-target split where flutter/web/js/
  is the runtime owner on Web rather than wasm-compiled Rust.
- 38 new i18n strings in src/lang/en.rs with Chinese
  translations in src/lang/cn.rs.

Refs discussion #1933.
2026-04-28 15:48:12 +08:00
158 changed files with 7580 additions and 14456 deletions

View File

@@ -25,7 +25,7 @@ jobs:
}
steps:
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@v4
with:
submodules: recursive
@@ -49,25 +49,25 @@ jobs:
wget
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ env.RUST_VERSION }}
targets: ${{ matrix.job.target }}
components: "rustfmt"
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- uses: Swatinem/rust-cache@v2
with:
prefix-key: bridge-${{ matrix.job.os }}
- name: Cache Bridge
id: cache-bridge
uses: actions/cache@6f8efc29b200d32929f49075959781ed54ec270c # v3
uses: actions/cache@v3
with:
path: /tmp/flutter_rust_bridge
key: vcpkg-${{ matrix.job.arch }}
- name: Install flutter
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
@@ -86,7 +86,7 @@ jobs:
cp ./flutter/macos/Runner/bridge_generated.h ./flutter/ios/Runner/bridge_generated.h
- name: Upload Artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@master
with:
name: bridge-artifact
path: |

View File

@@ -29,13 +29,13 @@ jobs:
# name: Ensure 'cargo fmt' has been run
# runs-on: ubuntu-20.04
# steps:
# - uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af # v1
# - uses: actions-rs/toolchain@v1
# with:
# toolchain: stable
# default: true
# profile: minimal
# components: rustfmt
# - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
# - uses: actions/checkout@v3
# - run: cargo fmt -- --check
# min_version:
@@ -43,24 +43,24 @@ jobs:
# runs-on: ubuntu-20.04
# steps:
# - name: Checkout source code
# uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
# uses: actions/checkout@v3
# with:
# submodules: recursive
# - name: Install rust toolchain (v${{ env.MIN_SUPPORTED_RUST_VERSION }})
# uses: actions-rs/toolchain@16499b5e05bf2e26879000db0c1d13f7e13fa3af # v1
# uses: actions-rs/toolchain@v1
# with:
# toolchain: ${{ env.MIN_SUPPORTED_RUST_VERSION }}
# default: true
# profile: minimal # minimal component installation (ie, no documentation)
# components: clippy
# - name: Run clippy (on minimum supported rust version to prevent warnings we can't fix)
# uses: actions-rs/cargo@844f36862e911db73fe0815f00a4a2602c279505 # v1
# uses: actions-rs/cargo@v1
# with:
# command: clippy
# args: --locked --all-targets --all-features -- --allow clippy::unknown_clippy_lints
# - name: Run tests
# uses: actions-rs/cargo@844f36862e911db73fe0815f00a4a2602c279505 # v1
# uses: actions-rs/cargo@v1
# with:
# command: test
# args: --locked
@@ -86,9 +86,9 @@ jobs:
steps:
- name: Free Disk Space (Ubuntu)
if: runner.os == 'Linux'
# jlumbroso/free-disk-space@v1.3.1 is used in .github\workflows\flutter-build.yml
# 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@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
uses: jlumbroso/free-disk-space@v1.3.1
with:
tool-cache: false
android: true
@@ -99,14 +99,14 @@ jobs:
swap-storage: false
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
uses: actions/github-script@v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@v4
with:
submodules: recursive
@@ -145,7 +145,7 @@ jobs:
esac
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
uses: lukka/run-vcpkg@v11
with:
vcpkgDirectory: /opt/artifacts/vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
@@ -156,7 +156,7 @@ jobs:
shell: bash
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
uses: dtolnay/rust-toolchain@v1
with:
toolchain: stable
targets: ${{ matrix.job.target }}
@@ -172,10 +172,10 @@ jobs:
cargo -V
rustc -V
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- uses: Swatinem/rust-cache@v2
- name: Build
uses: actions-rs/cargo@844f36862e911db73fe0815f00a4a2602c279505 # v1
uses: actions-rs/cargo@v1
with:
use-cross: ${{ matrix.job.use-cross }}
command: build
@@ -243,7 +243,7 @@ jobs:
echo "CARGO_TEST_OPTIONS=${CARGO_TEST_OPTIONS}" >> $GITHUB_OUTPUT
- name: Run tests
uses: actions-rs/cargo@844f36862e911db73fe0815f00a4a2602c279505 # v1
uses: actions-rs/cargo@v1
with:
use-cross: ${{ matrix.job.use-cross }}
command: test

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Clear cache
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
uses: actions/github-script@v7
with:
script: |
console.log("About to clear")
@@ -30,7 +30,7 @@ jobs:
console.log("Clear completed")
- name: Purge cache # Above seems not clear thouroughly, so add this to double clear
uses: MyAlbum/purge-cache@881eb5957687193fa612bf74c0042adc78ea5e54 # v2
uses: MyAlbum/purge-cache@v2
with:
accessed: true # Purge caches by their last accessed time (default)
created: false # Purge caches by their created time (default)

View File

@@ -31,7 +31,7 @@ jobs:
shell: bash
- name: Publish RustDesk version file
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: "fdroid-version"

View File

@@ -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.7"
VERSION: "1.4.6"
NDK_VERSION: "r28c"
#signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
@@ -81,30 +81,30 @@ jobs:
# - { target: aarch64-pc-windows-msvc, os: windows-2022, arch: aarch64 }
steps:
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
uses: actions/github-script@v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@v4
with:
submodules: recursive
- name: Restore bridge files
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: bridge-artifact
path: ./
- name: Install LLVM and Clang
uses: KyleMayes/install-llvm-action@1a3da29f56261a1e1f937ec88f0856a9b8321d7e # v1
uses: KyleMayes/install-llvm-action@v1
with:
version: ${{ env.LLVM_VERSION }}
- name: Install flutter
uses: subosito/flutter-action@2783a3f08e1baf891508463f8c6653c258246225 # v2.12.0; https://github.com/subosito/flutter-action/issues/277
uses: subosito/flutter-action@v2.12.0 #https://github.com/subosito/flutter-action/issues/277
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
@@ -126,18 +126,18 @@ jobs:
[[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply flutter_3.24.4_dropdown_menu_enableFilter.diff
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ env.SCITER_RUST_VERSION }}
targets: ${{ matrix.job.target }}
components: "rustfmt"
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- uses: Swatinem/rust-cache@v2
with:
prefix-key: ${{ matrix.job.os }}
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
uses: lukka/run-vcpkg@v11
with:
vcpkgDirectory: C:\vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
@@ -220,7 +220,7 @@ jobs:
fi
- name: Download RustDeskTempTopMostWindow artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
if: ${{ inputs.upload-artifact }}
with:
name: topmostwindow-artifacts
@@ -228,7 +228,7 @@ jobs:
- name: Upload unsigned
if: env.UPLOAD_ARTIFACT == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@master
with:
name: rustdesk-unsigned-windows-${{ matrix.job.arch }}
path: rustdesk
@@ -253,7 +253,7 @@ jobs:
mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.exe
- name: Add MSBuild to PATH
uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2
uses: microsoft/setup-msbuild@v2
- name: Build msi
if: env.UPLOAD_ARTIFACT == 'true'
@@ -272,7 +272,7 @@ jobs:
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@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
if: env.UPLOAD_ARTIFACT == 'true'
with:
prerelease: true
@@ -302,35 +302,35 @@ jobs:
# - { target: aarch64-pc-windows-msvc, os: windows-2022 }
steps:
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
uses: actions/github-script@v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install LLVM and Clang
uses: rustdesk-org/install-llvm-action-32bit@6aa7d9ad3df84dff01cd4596dd0fc880a7f47fce # no release tag; commit 2026-05-26
uses: rustdesk-org/install-llvm-action-32bit@master
with:
version: ${{ env.LLVM_VERSION }}
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
uses: dtolnay/rust-toolchain@v1
with:
toolchain: nightly-2023-10-13-${{ matrix.job.target }} # must use nightly here, because of abi_thiscall feature required
targets: ${{ matrix.job.target }}
components: "rustfmt"
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- uses: Swatinem/rust-cache@v2
with:
prefix-key: ${{ matrix.job.os }}-sciter
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
uses: lukka/run-vcpkg@v11
with:
vcpkgDirectory: C:\vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
@@ -363,8 +363,7 @@ jobs:
python3 res/inline-sciter.py
# Patch sciter x86
sed -i 's/branch = "dyn"/branch = "dyn_x86"/g' ./Cargo.toml
cargo update -p sciter-rs --precise 674e07d3066ca9a92ced3816203ab6b652629d1e
cargo build --locked --features inline,vram,hwcodec --release --bins
cargo build --features inline,vram,hwcodec --release --bins
mkdir -p ./Release
mv ./target/release/rustdesk.exe ./Release/rustdesk.exe
curl -LJ -o ./Release/sciter.dll https://github.com/c-smile/sciter-sdk/raw/master/bin.win/x32/sciter.dll
@@ -395,7 +394,7 @@ jobs:
- name: Upload unsigned
if: env.UPLOAD_ARTIFACT == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@master
with:
name: rustdesk-unsigned-windows-${{ matrix.job.arch }}
path: Release
@@ -425,7 +424,7 @@ jobs:
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@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
if: env.UPLOAD_ARTIFACT == 'true'
with:
prerelease: true
@@ -450,7 +449,7 @@ jobs:
}
steps:
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
uses: actions/github-script@v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
@@ -460,12 +459,12 @@ jobs:
run: |
brew install nasm yasm
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install flutter
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
@@ -476,7 +475,7 @@ jobs:
[[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
uses: lukka/run-vcpkg@v11
with:
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
doNotCache: false
@@ -500,19 +499,19 @@ jobs:
shell: bash
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ env.RUST_VERSION }}
targets: ${{ matrix.job.target }}
components: "rustfmt"
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- uses: Swatinem/rust-cache@v2
with:
prefix-key: rustdesk-lib-cache-ios
key: ${{ matrix.job.target }}
- name: Restore bridge files
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: bridge-artifact
path: ./
@@ -520,10 +519,10 @@ jobs:
- name: Build rustdesk lib
run: |
rustup target add ${{ matrix.job.target }}
cargo build --locked --features flutter,hwcodec --release --target aarch64-apple-ios --lib
cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib
- name: Upload liblibrustdesk.a Artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@master
with:
name: liblibrustdesk.a
path: target/aarch64-apple-ios/release/liblibrustdesk.a
@@ -538,14 +537,14 @@ jobs:
# - name: Upload Artifacts
# # if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
# uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
# uses: actions/upload-artifact@master
# with:
# name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
# path: flutter/build/ios/ipa/*.ipa
# - name: Publish ipa package
# # if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
# uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
# uses: softprops/action-gh-release@v1
# with:
# prerelease: true
# tag_name: ${{ env.TAG_NAME }}
@@ -578,20 +577,20 @@ jobs:
}
steps:
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
uses: actions/github-script@v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@v4
with:
submodules: recursive
- name: Import the codesign cert
if: env.MACOS_P12_BASE64 != null
uses: apple-actions/import-codesign-certs@253ddeeac23f2bdad1646faac5c8c2832e800071 # v1
uses: apple-actions/import-codesign-certs@v1
with:
p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }}
p12-password: ${{ secrets.MACOS_P12_PASSWORD }}
@@ -605,7 +604,7 @@ jobs:
- name: Import notarize key
if: env.MACOS_P12_BASE64 != null
uses: timheuer/base64-to-file@adaa40c0c581f276132199d4cf60afa07ce60eac # v1.2
uses: timheuer/base64-to-file@v1.2
with:
# https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling
fileName: rustdesk.json
@@ -644,7 +643,7 @@ jobs:
nasm --version
- name: Install flutter
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
@@ -663,24 +662,24 @@ jobs:
grep -n '_setFramesEnabledState(false);' ../packages/flutter/lib/src/scheduler/binding.dart
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ env.MAC_RUST_VERSION }}
targets: ${{ matrix.job.target }}
components: "rustfmt"
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- uses: Swatinem/rust-cache@v2
with:
prefix-key: ${{ matrix.job.os }}
- name: Restore bridge files
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: bridge-artifact
path: ./
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
uses: lukka/run-vcpkg@v11
with:
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
doNotCache: false
@@ -732,7 +731,7 @@ jobs:
- name: Upload unsigned macOS app
if: env.UPLOAD_ARTIFACT == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@master
with:
name: rustdesk-unsigned-macos-${{ matrix.job.arch }}
path: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.dmg # can not upload the directory directly or tar.gz, which destroy the link structure, causing the codesign failed
@@ -764,7 +763,7 @@ jobs:
- name: Publish DMG package
if: env.UPLOAD_ARTIFACT == 'true'
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
@@ -780,25 +779,25 @@ jobs:
if: ${{ inputs.upload-artifact }}
steps:
- name: Download artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: rustdesk-unsigned-macos-x86_64
path: ./
- name: Download Artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: rustdesk-unsigned-macos-aarch64
path: ./
- name: Download Artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: rustdesk-unsigned-windows-x86_64
path: ./windows-x86_64/
- name: Download Artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: rustdesk-unsigned-windows-x86
path: ./windows-x86/
@@ -808,7 +807,7 @@ jobs:
tar czf rustdesk-${{ env.VERSION }}-unsigned.tar.gz *.dmg windows-x86_64 windows-x86
- name: Publish unsigned app
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
@@ -845,7 +844,7 @@ jobs:
}
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: false
@@ -856,7 +855,7 @@ jobs:
swap-storage: false
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
uses: actions/github-script@v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
@@ -898,12 +897,12 @@ jobs:
wget
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install flutter
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ env.ANDROID_FLUTTER_VERSION }}
@@ -913,14 +912,14 @@ jobs:
cd $(dirname $(dirname $(which flutter)))
[[ "3.24.5" == ${{env.ANDROID_FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
- uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1
- uses: nttld/setup-ndk@v1
id: setup-ndk
with:
ndk-version: ${{ env.NDK_VERSION }}
add-to-path: true
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
uses: lukka/run-vcpkg@v11
with:
vcpkgDirectory: /opt/artifacts/vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
@@ -955,18 +954,18 @@ jobs:
shell: bash
- name: Restore bridge files
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: bridge-artifact
path: ./
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ env.RUST_VERSION }}
components: "rustfmt"
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- uses: Swatinem/rust-cache@v2
with:
prefix-key: rustdesk-lib-cache-android # TODO: drop '-android' part after caches are invalidated
key: ${{ matrix.job.target }}
@@ -1002,7 +1001,7 @@ jobs:
esac
- name: Upload Rustdesk library to Artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@master
with:
name: librustdesk.so.${{ matrix.job.target }}
path: ./target/${{ matrix.job.target }}/release/liblibrustdesk.so
@@ -1067,7 +1066,7 @@ jobs:
echo "ANDROID_SIGN_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
echo Last build tool version is: $BUILD_TOOL_VERSION
- uses: r0adkll/sign-android-release@349ebdef58775b1e0d8099458af0816dc79b6407 # v1
- uses: r0adkll/sign-android-release@v1
name: Sign app APK
if: env.ANDROID_SIGNING_KEY != null
id: sign-rustdesk
@@ -1083,14 +1082,14 @@ jobs:
- name: Upload Artifacts
if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@master
with:
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}}
- name: Publish signed apk package
if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
@@ -1099,7 +1098,7 @@ jobs:
- name: Publish unsigned apk package
if: env.ANDROID_SIGNING_KEY == null && env.UPLOAD_ARTIFACT == 'true'
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
@@ -1117,7 +1116,7 @@ jobs:
suffix: ""
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
uses: jlumbroso/free-disk-space@main
with:
tool-cache: false
android: false
@@ -1128,7 +1127,7 @@ jobs:
swap-storage: false
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
uses: actions/github-script@v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
@@ -1170,12 +1169,12 @@ jobs:
wget
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install flutter
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ env.ANDROID_FLUTTER_VERSION }}
@@ -1186,32 +1185,32 @@ jobs:
[[ "3.24.5" == ${{env.ANDROID_FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
- name: Restore bridge files
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: bridge-artifact
path: ./
- name: Download Rustdesk library from Artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: librustdesk.so.aarch64-linux-android
path: ./flutter/android/app/src/main/jniLibs/arm64-v8a
- name: Download Rustdesk library from Artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: librustdesk.so.armv7-linux-androideabi
path: ./flutter/android/app/src/main/jniLibs/armeabi-v7a
- name: Download Rustdesk library from Artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: librustdesk.so.x86_64-linux-android
path: ./flutter/android/app/src/main/jniLibs/x86_64
- name: Download Rustdesk library from Artifacts
if: ${{ env.reltype == 'debug' }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: librustdesk.so.i686-linux-android
path: ./flutter/android/app/src/main/jniLibs/x86
@@ -1251,7 +1250,7 @@ jobs:
echo "ANDROID_SIGN_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
echo Last build tool version is: $BUILD_TOOL_VERSION
- uses: r0adkll/sign-android-release@349ebdef58775b1e0d8099458af0816dc79b6407 # v1
- uses: r0adkll/sign-android-release@v1
name: Sign app APK
if: env.ANDROID_SIGNING_KEY != null
id: sign-rustdesk
@@ -1267,14 +1266,14 @@ jobs:
- name: Upload Artifacts
if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@master
with:
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk
path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}}
- name: Publish signed apk package
if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
@@ -1283,7 +1282,7 @@ jobs:
- name: Publish unsigned apk package
if: env.ANDROID_SIGNING_KEY == null && env.UPLOAD_ARTIFACT == 'true'
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
@@ -1317,7 +1316,7 @@ jobs:
}
steps:
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
uses: actions/github-script@v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
@@ -1335,13 +1334,13 @@ jobs:
fi
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@v4
with:
submodules: recursive
- name: Set Swap Space
if: ${{ matrix.job.arch == 'x86_64' }}
uses: pierotofy/set-swap-space@49819abfb41bd9b44fb781159c033dba90353a7c # v1.0
uses: pierotofy/set-swap-space@master
with:
swap-size-gb: 12
@@ -1351,7 +1350,7 @@ jobs:
free -m
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
uses: dtolnay/rust-toolchain@v1
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
with:
toolchain: ${{ env.RUST_VERSION }}
@@ -1370,14 +1369,14 @@ jobs:
- name: Restore bridge files
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: bridge-artifact
path: ./
- name: Setup vcpkg with Github Actions binary cache
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
uses: lukka/run-vcpkg@v11
with:
vcpkgDirectory: /opt/artifacts/vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
@@ -1405,12 +1404,12 @@ jobs:
- name: Restore bridge files
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: bridge-artifact
path: ./
- uses: rustdesk-org/run-on-arch-action@d3fcfbb632b84cf7f6bc772bfaaa2c2f4f8789a8 # no release tag; commit 2026-05-26
- uses: rustdesk-org/run-on-arch-action@amd64-support
name: Build rustdesk
id: vcpkg
if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true'
@@ -1492,7 +1491,7 @@ jobs:
export JOBS=""
fi
echo $JOBS
cargo build --locked --lib $JOBS --features hwcodec,flutter,unix-file-copy-paste --release
cargo build --lib $JOBS --features hwcodec,flutter,unix-file-copy-paste --release
rm -rf target/release/deps target/release/build
rm -rf ~/.cargo
@@ -1584,7 +1583,7 @@ jobs:
- name: Publish debian/rpm package
if: env.UPLOAD_ARTIFACT == 'true'
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
@@ -1593,7 +1592,7 @@ jobs:
rustdesk-*.rpm
- name: Upload deb
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@master
if: env.UPLOAD_ARTIFACT == 'true'
with:
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb
@@ -1612,7 +1611,7 @@ jobs:
- name: Build archlinux package
if: matrix.job.arch == 'x86_64' && env.UPLOAD_ARTIFACT == 'true'
uses: rustdesk-org/arch-makepkg-action@04200739ed1d0bf6f2188b6736b26a767c57a7f9 # no release tag; commit 2026-05-26
uses: rustdesk-org/arch-makepkg-action@master
with:
packages:
scripts: |
@@ -1620,7 +1619,7 @@ jobs:
- name: Publish archlinux package
if: matrix.job.arch == 'x86_64' && env.UPLOAD_ARTIFACT == 'true'
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
@@ -1658,14 +1657,14 @@ jobs:
}
steps:
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
uses: actions/github-script@v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@v4
with:
submodules: recursive
@@ -1683,7 +1682,7 @@ jobs:
free -m
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ env.SCITER_RUST_VERSION }}
targets: ${{ matrix.job.target }}
@@ -1694,7 +1693,7 @@ jobs:
RUST_TOOLCHAIN_VERSION=$(cargo --version | awk '{print $2}')
echo "RUST_TOOLCHAIN_VERSION=$RUST_TOOLCHAIN_VERSION" >> $GITHUB_ENV
- uses: rustdesk-org/run-on-arch-action@d3fcfbb632b84cf7f6bc772bfaaa2c2f4f8789a8 # no release tag; commit 2026-05-26
- uses: rustdesk-org/run-on-arch-action@amd64-support
name: Build rustdesk sciter binary for ${{ matrix.job.arch }}
id: vcpkg
with:
@@ -1822,7 +1821,7 @@ jobs:
# build rustdesk
python3 ./res/inline-sciter.py
export CARGO_INCREMENTAL=0
cargo build --locked --features inline${{ matrix.job.extra_features }} --release --bins --jobs 1
cargo build --features inline${{ matrix.job.extra_features }} --release --bins --jobs 1
# make debian package
mkdir -p ./Release
mv ./target/release/rustdesk ./Release/rustdesk
@@ -1840,7 +1839,7 @@ jobs:
- name: Publish debian package
if: env.UPLOAD_ARTIFACT == 'true'
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
@@ -1848,7 +1847,7 @@ jobs:
rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb
- name: Upload deb
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@master
if: env.UPLOAD_ARTIFACT == 'true'
with:
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb
@@ -1867,12 +1866,12 @@ jobs:
- { target: aarch64-unknown-linux-gnu, arch: aarch64 }
steps:
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@v4
with:
submodules: recursive
- name: Download Binary
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb
path: .
@@ -1897,7 +1896,7 @@ jobs:
- name: Publish appimage package
if: env.UPLOAD_ARTIFACT == 'true'
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
@@ -1940,12 +1939,12 @@ jobs:
}
steps:
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@v4
with:
submodules: recursive
- name: Download Binary
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@master
with:
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.deb
path: .
@@ -1954,7 +1953,7 @@ jobs:
run: |
mv rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.deb flatpak/rustdesk.deb
- uses: rustdesk-org/run-on-arch-action@d3fcfbb632b84cf7f6bc772bfaaa2c2f4f8789a8 # no release tag; commit 2026-05-26
- uses: rustdesk-org/run-on-arch-action@amd64-support
name: Build rustdesk flatpak package for ${{ matrix.job.arch }}
id: flatpak
with:
@@ -1982,7 +1981,7 @@ jobs:
flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.flatpak com.rustdesk.RustDesk
- name: Publish flatpak package
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
@@ -2001,7 +2000,7 @@ jobs:
RELEASE_NAME: web-basic
steps:
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
uses: actions/checkout@v4
with:
submodules: recursive
@@ -2011,7 +2010,7 @@ jobs:
sudo apt-get install -y wget npm
- name: Install flutter
uses: subosito/flutter-action@2783a3f08e1baf891508463f8c6653c258246225 # v2.12.0; https://github.com/subosito/flutter-action/issues/277
uses: subosito/flutter-action@v2.12.0 #https://github.com/subosito/flutter-action/issues/277
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
@@ -2055,7 +2054,7 @@ jobs:
- name: Publish web
if: env.UPLOAD_ARTIFACT == 'true'
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}

View File

@@ -17,7 +17,7 @@ env:
TAG_NAME: "nightly"
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
VERSION: "1.4.7"
VERSION: "1.4.6"
NDK_VERSION: "r26d"
#signing keys env variable checks
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
@@ -79,21 +79,21 @@ jobs:
}
steps:
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
uses: actions/github-script@v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Checkout source code
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
uses: actions/checkout@v3
with:
ref: ${{ matrix.job.ref }}
submodules: recursive
- name: Import the codesign cert
if: env.MACOS_P12_BASE64 != null
uses: apple-actions/import-codesign-certs@253ddeeac23f2bdad1646faac5c8c2832e800071 # v1
uses: apple-actions/import-codesign-certs@v1
with:
p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }}
p12-password: ${{ secrets.MACOS_P12_PASSWORD }}
@@ -107,7 +107,7 @@ jobs:
- name: Import notarize key
if: env.MACOS_P12_BASE64 != null
uses: timheuer/base64-to-file@adaa40c0c581f276132199d4cf60afa07ce60eac # v1.2
uses: timheuer/base64-to-file@v1.2
with:
# https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling
fileName: rustdesk.json
@@ -129,19 +129,19 @@ jobs:
brew install llvm create-dmg nasm pkg-config
- name: Install flutter
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ matrix.job.flutter }}
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ env.RUST_VERSION }}
targets: ${{ matrix.job.target }}
components: "rustfmt"
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- uses: Swatinem/rust-cache@v2
with:
prefix-key: ${{ matrix.job.os }}
@@ -156,7 +156,7 @@ jobs:
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
uses: lukka/run-vcpkg@v11
with:
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
@@ -165,7 +165,7 @@ jobs:
$VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
- name: Restore from cache and install vcpkg
uses: lukka/run-vcpkg@8a5116de2b552d6fc8894e9774aacaf2e5db4823 # v7 2026-05-26
uses: lukka/run-vcpkg@v7
if: false
with:
setupOnly: true
@@ -222,7 +222,7 @@ jobs:
done
- name: Publish DMG package
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
@@ -247,7 +247,7 @@ jobs:
}
steps:
- name: Checkout source code
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3
uses: actions/checkout@v3
with:
ref: ${{ matrix.job.ref }}
submodules: recursive
@@ -290,13 +290,13 @@ jobs:
wget
- name: Install flutter
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
uses: dtolnay/rust-toolchain@v1
with:
toolchain: ${{ env.RUST_VERSION }}
components: "rustfmt"
@@ -310,14 +310,14 @@ jobs:
pushd flutter ; flutter pub get ; popd
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
- uses: nttld/setup-ndk@ed92fe6cadad69be94a966a7ee3271275e62f779 # v1
- uses: nttld/setup-ndk@v1
id: setup-ndk
with:
ndk-version: ${{ env.NDK_VERSION }}
add-to-path: true
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
uses: lukka/run-vcpkg@v11
with:
vcpkgDirectory: /opt/artifacts/vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
@@ -395,7 +395,7 @@ jobs:
mkdir -p signed-apk; pushd signed-apk
mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk ./rustdesk-test-${{ matrix.job.ref }}-${{ matrix.job.ndk }}.apk
- uses: r0adkll/sign-android-release@349ebdef58775b1e0d8099458af0816dc79b6407 # v1
- uses: r0adkll/sign-android-release@v1
name: Sign app APK
if: env.ANDROID_SIGNING_KEY != null
id: sign-rustdesk
@@ -410,7 +410,7 @@ jobs:
BUILD_TOOLS_VERSION: "30.0.2"
- name: Publish signed apk package
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
uses: softprops/action-gh-release@v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}

View File

@@ -39,7 +39,7 @@ jobs:
build_output_dir: RustDeskTempTopMostWindow/WindowInjection/${{ inputs.platform }}/${{ inputs.configuration }}
steps:
- name: Add MSBuild to PATH
uses: microsoft/setup-msbuild@6fb02220983dee41ce7ae257b6f4d8f9bf5ed4ce # v2
uses: microsoft/setup-msbuild@v2
- name: Download the source code
run: |
@@ -52,7 +52,7 @@ jobs:
msbuild ${{ env.project_path }} -p:Configuration=${{ inputs.configuration }} -p:Platform=${{ inputs.platform }} /p:TargetVersion=${{ inputs.target_version }}
- name: Archive build artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@master
if: ${{ inputs.upload-artifact }}
with:
name: topmostwindow-artifacts

View File

@@ -1,85 +0,0 @@
name: wf-cliprdr CI
on:
workflow_dispatch:
pull_request:
paths:
- "libs/clipboard/src/windows/**"
- "tests/test_invariant_wf_cliprdr.c"
- ".github/workflows/wf-cliprdr-ci.yml"
push:
branches:
- master
paths:
- "libs/clipboard/src/windows/**"
- "tests/test_invariant_wf_cliprdr.c"
- ".github/workflows/wf-cliprdr-ci.yml"
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: wf_cliprdr invariant test
runs-on: windows-2022
steps:
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- name: Set up MSVC
uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756
with:
arch: x64
- name: Setup vcpkg with GitHub Actions binary cache
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
with:
vcpkgDirectory: C:\vcpkg
doNotCache: false
- name: Install vcpkg dependency
shell: pwsh
run: |
& "$env:VCPKG_ROOT\vcpkg.exe" install check:x64-windows --classic --x-install-root="$env:VCPKG_ROOT\installed"
- name: Build test
shell: pwsh
run: |
$testRoot = Join-Path $env:GITHUB_WORKSPACE 'build\wf-cliprdr'
New-Item -ItemType Directory -Force $testRoot | Out-Null
$testSource = (($env:GITHUB_WORKSPACE -replace '\\', '/') + '/tests/test_invariant_wf_cliprdr.c')
$cmakeLists = @(
'cmake_minimum_required(VERSION 3.20)'
'project(test_invariant_wf_cliprdr C)'
''
'set(CMAKE_C_STANDARD 11)'
'set(CMAKE_C_STANDARD_REQUIRED ON)'
'set(CMAKE_C_EXTENSIONS OFF)'
''
'find_package(check CONFIG REQUIRED)'
''
'add_executable(test_invariant_wf_cliprdr'
' "TEST_SOURCE"'
')'
''
'target_link_libraries(test_invariant_wf_cliprdr PRIVATE'
' $<$<TARGET_EXISTS:Check::check>:Check::check>'
' $<$<NOT:$<TARGET_EXISTS:Check::check>>:Check::checkShared>'
')'
) -join [Environment]::NewLine
$cmakeLists.Replace('TEST_SOURCE', $testSource) | Set-Content -NoNewline (Join-Path $testRoot 'CMakeLists.txt')
cmake -S $testRoot -B (Join-Path $testRoot 'out') -G "Visual Studio 17 2022" -A x64 -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_ROOT\scripts\buildsystems\vcpkg.cmake" -DVCPKG_TARGET_TRIPLET=x64-windows
cmake --build (Join-Path $testRoot 'out') --config Release
- name: Run test
shell: pwsh
run: .\build\wf-cliprdr\out\Release\test_invariant_wf_cliprdr.exe

4
.gitignore vendored
View File

@@ -55,4 +55,6 @@ examples/**/target/
vcpkg_installed
flutter/lib/generated_plugin_registrant.dart
libsciter.dylib
flutter/web/
flutter/web/
# Local git worktrees
.worktrees/

View File

@@ -53,6 +53,30 @@
* Use `spawn_blocking` or dedicated threads for blocking work.
* Do not use `std::thread::sleep()` in async code.
## Flutter Rust Bridge
* Do **not** run `flutter_rust_bridge_codegen` — it requires a specific pinned version that is not easy to set up locally.
* When adding new FFI functions in `src/flutter_ffi.rs`, hand-write the corresponding Dart wrappers instead of regenerating.
* Web bridge (committed): edit `flutter/lib/web/bridge.dart` directly. Follow the existing patterns there for `SyncReturn<T>` / `Future<T>` and the `dart:js` glue.
* Native bridge (`flutter/lib/generated_bridge.dart`, `src/bridge_generated.rs`, `src/bridge_generated.io.rs`): these are gitignored and regenerated by the project's CI codegen. Manually editing them locally is fine for development testing, but those edits do not persist into commits.
## Web (Flutter Web) Architecture
Flutter Web in this repo is **not** "Dart compiled to JS via Flutter alone". The runtime is split:
* **Native targets (Win/Mac/Linux/Android/iOS)**: Rust drives sessions via `flutter_rust_bridge`; Dart only renders UI.
* **Web target**: Rust does **not** run. There is a separate hand-written TypeScript / JavaScript client at `flutter/web/js/` (gitignored — not present in this repo, lives in the maintainer's local tree). It owns connection, codec, keyboard, clipboard, etc. — basically a JS port of the Rust client. The Dart UI talks to it through `flutter/lib/web/bridge.dart`, which uses `dart:js` to call JS-side functions and to register Dart-side callbacks on `window.*`.
Implications when adding any session-runtime feature (keyboard, clipboard, audio, …):
* The Rust implementation in `src/` is for **native only**. Don't try to compile it to wasm.
* The matching Web-side logic must be written in TS/JS under `flutter/web/js/src/`. It's a translation of the Rust logic, usually simpler — Web is single-window, so any per-session-id plumbing in Rust collapses to a single global on Web.
* `flutter/lib/web/bridge.dart` is the only place where Dart sees JS. Other Dart code stays platform-agnostic and goes through `bind`. Don't sprinkle `if (isWeb)` runtime branches in shared Dart files to call Web-specific logic — put the platform divergence in the bridge.
* For JS → Dart events (e.g., a Web matcher firing), the convention is: Dart sets `js.context['onFooBar'] = (...) {...}` once at startup (typically in `mainInit`); the JS side calls `window.onFooBar(...)`. See `onLoadAbFinished`, `onLoadGroupFinished` for reference.
* The maintainer cannot easily run `flutter_rust_bridge_codegen`, so when a new FFI function lands in `src/flutter_ffi.rs`:
1. add the Web counterpart to `flutter/lib/web/bridge.dart` by hand;
2. note that on the Web target it may need to be a no-op or a JS bridge call rather than a real Rust invocation.
## Editing Hygiene
* Change only what is required.
@@ -60,27 +84,3 @@
* Do not refactor unrelated code.
* Do not make formatting-only changes.
* Keep naming/style consistent with nearby code.
## Localization (`src/lang/*.rs`)
Each file is a `HashMap<key, translation>`. Layout:
* `template.rs` is the master list of every key. **Never edit it** as part of translation work.
* `en.rs` holds only the keys whose English display text differs from the key itself.
* Every other file (`de.rs`, `fr.rs`, …) carries the full key set; an untranslated entry has an empty value: `("key", "")`.
### Finding the English source for a key
When filling an empty entry, determine the source English text with this rule:
* If `key` exists in `en.rs` **with a non-empty value**, that value is the source text (look it up in `en.rs`).
* Otherwise the **key string itself is the source text** (the key is already plain English).
Then translate that source into the file's target language (infer the language from the file's existing non-empty entries / filename).
### Translation hygiene
* Only fill empty values. Never change keys, and never touch existing non-empty translations.
* Preserve placeholders (`{}`) and escape sequences (`\n`, `\"`) exactly as in the source.
* Do not translate brand or technical tokens: `RustDesk`, `Socks5`, `TLS`, `UAC`, `Wayland`, `X11`, `TCP`, `UDP`, `2FA`, `RDP`, `D3D`, etc.
* Copy URL values (e.g. `doc_*` keys) verbatim from `en.rs`.

8
Cargo.lock generated
View File

@@ -5996,8 +5996,8 @@ dependencies = [
[[package]]
name = "parity-tokio-ipc"
version = "0.7.3-6"
source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#d0ae39bffe5d5a3e8d82a1b6bcb1ca5a9b2f1c01"
version = "0.7.3-5"
source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#c8c8bbcbabf9be1201c53afb0269b92b9b02d291"
dependencies = [
"futures",
"libc",
@@ -7270,7 +7270,7 @@ dependencies = [
[[package]]
name = "rustdesk"
version = "1.4.7"
version = "1.4.6"
dependencies = [
"android-wakelock",
"android_logger",
@@ -7385,7 +7385,7 @@ dependencies = [
[[package]]
name = "rustdesk-portable-packer"
version = "1.4.7"
version = "1.4.6"
dependencies = [
"brotli",
"dirs 5.0.1",

View File

@@ -1,6 +1,6 @@
[package]
name = "rustdesk"
version = "1.4.7"
version = "1.4.6"
authors = ["rustdesk <info@rustdesk.com>"]
edition = "2021"
build= "build.rs"

View File

@@ -18,7 +18,7 @@ AppDir:
id: rustdesk
name: rustdesk
icon: rustdesk
version: 1.4.7
version: 1.4.6
exec: usr/share/rustdesk/rustdesk
exec_args: $@
apt:

View File

@@ -18,7 +18,7 @@ AppDir:
id: rustdesk
name: rustdesk
icon: rustdesk
version: 1.4.7
version: 1.4.6
exec: usr/share/rustdesk/rustdesk
exec_args: $@
apt:

View File

@@ -172,7 +172,7 @@ def generate_build_script_for_docker():
# flutter_rust_bridge
dart pub global activate ffigen --version 5.0.1
pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 && popd
pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . --locked && popd
pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd
pushd flutter && flutter pub get && popd
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
# install vcpkg
@@ -299,7 +299,7 @@ Version: %s
Architecture: %s
Maintainer: rustdesk <info@rustdesk.com>
Homepage: https://rustdesk.com
Depends: libgtk-3-0t64 | libgtk-3-0, libxcb-randr0, libxdo3 | libxdo4, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2t64 | 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.
@@ -317,7 +317,7 @@ def ffi_bindgen_function_refactor():
def build_flutter_deb(version, features):
if not skip_cargo:
system2(f'cargo build --locked --features {features} --lib --release')
system2(f'cargo build --features {features} --lib --release')
ffi_bindgen_function_refactor()
os.chdir('flutter')
system2('flutter build linux --release')
@@ -405,7 +405,7 @@ def build_flutter_dmg(version, features):
if not skip_cargo:
# set minimum osx build target, now is 10.14, which is the same as the flutter xcode project
system2(
f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --locked --features {features} --release')
f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --release')
# copy dylib
system2(
"cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib")
@@ -422,7 +422,7 @@ def build_flutter_dmg(version, features):
def build_flutter_arch_manjaro(version, features):
if not skip_cargo:
system2(f'cargo build --locked --features {features} --lib --release')
system2(f'cargo build --features {features} --lib --release')
ffi_bindgen_function_refactor()
os.chdir('flutter')
system2('flutter build linux --release')
@@ -433,7 +433,7 @@ def build_flutter_arch_manjaro(version, features):
def build_flutter_windows(version, features, skip_portable_pack):
if not skip_cargo:
system2(f'cargo build --locked --features {features} --lib --release')
system2(f'cargo build --features {features} --lib --release')
if not os.path.exists("target/release/librustdesk.dll"):
print("cargo build failed, please check rust source code.")
exit(-1)
@@ -489,13 +489,13 @@ def main():
if windows:
# build virtual display dynamic library
os.chdir('libs/virtual_display/dylib')
system2('cargo build --locked --release')
system2('cargo build --release')
os.chdir('../../..')
if flutter:
build_flutter_windows(version, features, args.skip_portable_pack)
return
system2('cargo build --locked --release --features ' + features)
system2('cargo build --release --features ' + features)
# system2('upx.exe target/release/rustdesk.exe')
system2('mv target/release/rustdesk.exe target/release/RustDesk.exe')
pa = os.environ.get('P')
@@ -519,7 +519,7 @@ def main():
if flutter:
build_flutter_arch_manjaro(version, features)
else:
system2('cargo build --locked --release --features ' + features)
system2('cargo build --release --features ' + features)
system2('git checkout src/ui/common.tis')
system2('strip target/release/rustdesk')
system2('ln -s res/pacman_install && ln -s res/PKGBUILD')
@@ -528,7 +528,7 @@ def main():
version, version))
# pacman -U ./rustdesk.pkg.tar.zst
elif os.path.isfile('/usr/bin/yum'):
system2('cargo build --locked --release --features ' + features)
system2('cargo build --release --features ' + features)
system2('strip target/release/rustdesk')
system2(
"sed -i 's/Version: .*/Version: %s/g' res/rpm.spec" % version)
@@ -538,7 +538,7 @@ def main():
version, version))
# yum localinstall rustdesk.rpm
elif os.path.isfile('/usr/bin/zypper'):
system2('cargo build --locked --release --features ' + features)
system2('cargo build --release --features ' + features)
system2('strip target/release/rustdesk')
system2(
"sed -i 's/Version: .*/Version: %s/g' res/rpm-suse.spec" % version)
@@ -557,7 +557,7 @@ def main():
# 'mv target/release/bundle/deb/rustdesk*.deb ./flutter/rustdesk.deb')
build_flutter_deb(version, features)
else:
system2('cargo --locked bundle --release --features ' + features)
system2('cargo bundle --release --features ' + features)
if osx:
system2(
'strip target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk')

View File

@@ -1,10 +1,10 @@
<p align="center">
<img src="../res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
<a href="#빌드를_위한_원시_단계">빌드</a> •
<a href="#Docker로_빌드하는_방법">Docker</a> •
<a href="#파일_구조">구조</a> •
<a href="#스크린샷">스샷</a><br>
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-DA.md">Dansk</a>] | [<a href="README-GR.md">Ελληνικά</a>] | [<a href="README-TR.md">Türkçe</a>] | [<a href="README-NO.md">Norsk</a>] | [<a href="README-RO.md">Română</a>]<br>
<a href="#빌드를 위한 원시 단계">빌드</a> •
<a href="#Docker로 빌드하는 방법">Docker</a> •
<a href="#파일 구조">구조</a> •
<a href="#스크린샷">스샷</a><br>
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-DA.md">Dansk</a>] | [<a href="README-GR.md">Ελληνικά</a>] | [<a href="README-TR.md">Türkçe</a>] | [<a href="README-NO.md">Norsk</a>]<br>
<b>이 README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> 및 <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk 문서</a>를 귀하의 모국어로 번역하는 데 도움이 필요합니다</b>
</p>
@@ -46,9 +46,9 @@ Sciter 동적 라이브러리를 직접 다운로드하세요.
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
## 빌드를_위한_원시_단계
## 빌드를 위한 원시 단계
- Rust 개발 환경과 C++ 빌드 환경 준비
- Rust 개발 환경과 C++ 빌드 환경 준비합니다
- [vcpkg](https://github.com/microsoft/vcpkg)를 설치하고 `VCPKG_ROOT` 환경 변수를 올바르게 설정합니다
@@ -125,7 +125,7 @@ mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run
```
## Docker로_빌드하는_방법
## Docker로 빌드하는 방법
먼저 리포지토리를 복제하고 Docker 컨테이너를 빌드합니다:
@@ -156,7 +156,7 @@ target/release/rustdesk
RustDesk 리포지토리의 루트에서 이러한 명령을 실행하고 있는지 확인하세요. 그렇지 않으면 응용 프로그램이 필요한 리소스를 찾지 못할 수 있습니다. 또한 `install` 또는 `run` 과 같은 다른 cargo 하위 명령은 호스트가 아닌 컨테이너 내부에 프로그램을 설치하거나 실행하므로 현재 이 방법을 통해 지원되지 않는다는 점에 유의하세요.
## 파일_구조
## 파일 구조
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 비디오 코덱, 구성, tcp/udp wrapper, protobuf, 파일 전송을 위한 fs 함수 및 기타 유틸리티 함수
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 화면 캡쳐

View File

@@ -1,82 +1,55 @@
<p align="center">
<img src="../res/logo-header.svg" alt="RustDesk - Seu desktop remoto"><br>
<a href="#compilar">Compilar</a> •
<a href="#como-compilar-com-o-docker">Docker</a> •
<a href="#servidores-públicos-grátis">Servidores</a> •
<a href="#compilação-crua">Compilar</a> •
<a href="#como-compilar-com-docker">Docker</a> •
<a href="#estrutura-de-arquivos">Estrutura</a> •
<a href="#capturas-de-tela">Capturas de Tela</a><br>
[<a href="../README.md">Inglês</a>] | [<a href="docs/README-UA.md">Ucraniano</a>] | [<a href="docs/README-CS.md">Tcheco</a>] | [<a href="docs/README-ZH.md">Chinês</a>] | [<a href="docs/README-HU.md">Húngaro</a>] | [<a href="docs/README-ES.md">Espanhol</a>] | [<a href="docs/README-FA.md">Persa</a>] | [<a href="docs/README-FR.md">Frans</a>] | [<a href="docs/README-DE.md">Alemão</a>] | [<a href="docs/README-PL.md">Polonês</a>] | [<a href="docs/README-ID.md">Indonésio</a>] | [<a href="docs/README-FI.md">Finlandês</a>] | [<a href="docs/README-ML.md">Malaiala</a>] | [<a href="docs/README-JP.md">Japonês</a>] | [<a href="docs/README-NL.md">Holandês</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Russo</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">Coreano</a>] | [<a href="docs/README-AR.md">Árabe</a>] | [<a href="docs/README-VN.md">Vietnamita</a>] | [<a href="docs/README-DA.md">Dinamarquês</a>] | [<a href="docs/README-GR.md">Grego</a>] | [<a href="docs/README-TR.md">Turco</a>] | [<a href="docs/README-NO.md">Norueguês</a>] | [<a href="docs/README-RO.md">Romeno</a>]<br>
<b>Precisamos da sua ajuda para traduzir este README, a <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">Interface do RustDesk</a> e a <a href="https://github.com/rustdesk/doc.rustdesk.com">Documentação do RustDesk</a> para o seu idioma nativo</b>
<a href="#screenshots">Screenshots</a><br>
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
<b>Precisamos de sua ajuda para traduzir este README e a <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">UI do RustDesk</a> para sua língua nativa</b>
</p>
> [!Caution]
> **Aviso de Isenção de Responsabilidade por Uso Indevido:** <br>
> Os desenvolvedores do RustDesk não toleram ou apoiam qualquer uso antiético ou ilegal deste software. O uso indevido, como acesso não autorizado, controle ou invasão de privacidade, viola estritamente nossas diretrizes. Os autores não são responsáveis por qualquer uso indevido do aplicativo.
Converse conosco: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk)
[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Advanced%20Features-blue)](https://rustdesk.com/pricing.html)
[![RustDesk Server Pro](https://img.shields.io/badge/RustDesk%20Server%20Pro-Recursos%20Avan%C3%A7ados-blue)](https://rustdesk.com/pricing.html)
Mais uma solução de desktop remoto, escrita em Rust. Funciona imediatamente, sem necessidade de configuração. Você tem controle total dos seus dados, sem preocupações com segurança. Você pode usar nosso servidor de conexão/retransmissão (rendezvous/relay), [configurar o seu próprio](https://rustdesk.com/server) ou [escrever seu próprio servidor de conexão/retransmissão](https://github.com/rustdesk/rustdesk-server-demo).
Mais um software de desktop remoto, escrito em Rust. Funciona por padrão, sem necessidade de configuração. Você tem completo controle de seus dados, sem se preocupar com segurança. Você pode usar nossos servidores de rendezvous/relay, [configurar seu próprio](https://rustdesk.com/server), ou [escrever seu próprio servidor de rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo).
![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
RustDesk acolhe contribuições de todos. Leia [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) para ver como começar.
O RustDesk acolhe a contribuição de todos. Veja [CONTRIBUTING.md](docs/CONTRIBUTING.md) para ajuda em como começar.
[**Perguntas Frequentes (FAQ)**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
[**DOWNLOAD DOS BINÁRIOS**](https://github.com/rustdesk/rustdesk/releases)
[**VERSÕES NIGHTLY (EM DESENVOLVIMENTO)**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
[<img src="https://f-droid.org/badge/get-it-on.png"
alt="Baixe no F-Droid"
height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
[<img src="https://flathub.org/api/badge?svg&locale=en"
alt="Baixe no Flathub"
height="80">](https://flathub.org/apps/com.rustdesk.RustDesk)
[**DOWNLOAD DE BINÁRIOS**](https://github.com/rustdesk/rustdesk/releases)
## Dependências
As versões de desktop usam Flutter ou Sciter (descontinuado) para a interface gráfica (GUI). Este tutorial é apenas para o Sciter, por ser mais fácil e amigável para começar. Verifique nosso [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) para instruções de compilação da versão em Flutter.
Por favor, faça o download da biblioteca dinâmica do Sciter por conta própria.
Versões de desktop utilizam [sciter](https://sciter.com/) para a GUI, por favor baixe a biblioteca dinâmica sciter por conta própria.
[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) |
[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
## Passos básicos para compilar
## Compilação crua
- Prepare seu ambiente de desenvolvimento Rust e o ambiente de compilação C++
- Prepare seu ambiente de desenvolvimento Rust e ambiente de compilação C++
- Instale o [vcpkg](https://github.com/microsoft/vcpkg) e configure a variável de ambiente `VCPKG_ROOT` corretamente
- Instale [vcpkg](https://github.com/microsoft/vcpkg), e configure a variável de ambiente `VCPKG_ROOT` corretamente
- Windows: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static`
- Linux/macOS: `vcpkg install libvpx libyuv opus aom`
- Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
- Linux/MacOS: vcpkg install libvpx libyuv opus aom
- Execute `cargo run`
## [Compilar](https://rustdesk.com/docs/en/dev/build/)
## Como Compilar no Linux
## Como compilar no Linux
### Ubuntu 18 (Debian 10)
```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 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 pam-devel
sudo apt install -y 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
```
### 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 gstreamer1-devel gstreamer1-plugins-base-devel pam-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
```
### Arch (Manjaro)
@@ -85,7 +58,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
```
### Instalar o vcpkg
### Instale vcpkg
```sh
git clone https://github.com/microsoft/vcpkg
@@ -97,7 +70,7 @@ export VCPKG_ROOT=$HOME/vcpkg
vcpkg/vcpkg install libvpx libyuv opus aom
```
### Corrigir o libvpx (Para Fedora)
### Conserte libvpx (Para o Fedora)
```sh
cd vcpkg/buildtrees/libvpx/src
@@ -110,12 +83,12 @@ cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
cd
```
### Compilar
### Compile
```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
git clone --recurse-submodules https://github.com/rustdesk/rustdesk
git clone 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
@@ -123,57 +96,57 @@ mv libsciter-gtk.so target/debug
VCPKG_ROOT=$HOME/vcpkg cargo run
```
## Como compilar com o Docker
## Como compilar com Docker
Comece clonando o repositório e construindo o contêiner Docker:
Comece clonando o repositório e montando o container docker:
```sh
git clone https://github.com/rustdesk/rustdesk
cd rustdesk
git submodule update --init --recursive
docker build -t "rustdesk-builder" .
```
Depois, cada vez que precisar compilar o aplicativo, execute o seguinte comando:
Então, sempre que precisar compilar a aplicação, execute este comando:
```sh
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
```
Note que a primeira compilação pode demorar mais a que as dependências sejam armazenadas em cache; as compilações subsequentes serão mais rápidas. Além disso, se você precisar especificar argumentos diferentes para o comando de compilação, pode fazê-lo ao final do comando na posição `<ARGUMENTOS-OPCIONAIS>`. Por exemplo, se você quiser compilar uma versão de lançamento (release) otimizada, executaria o comando acima seguido de `--release`. O executável resultante estará disponível na pasta `target` do seu sistema e pode ser executado com:
Note que a primeira compilação pode demorar mais antes que as dependências sejam armazenadas em cache, as compilações subsequentes serão mais rápidas. Adicionalmente, se você precisar especificar argumentos diferentes para o comando de compilação, você pode fazê-lo ao final do comando na posição do `<OPTIONAL-ARGS>`. Por exemplo, se você gostaria de compilar uma versão de release otimizada, você executaria o comando acima seguido de `--release`. O executável gerado estará disponível no diretório alvo no seu sistema, e pode ser executado com:
```sh
target/debug/rustdesk
```
Ou, se estiver executando o executável de lançamento:
Ou, se estiver rodando um executável de release:
```sh
target/release/rustdesk
```
Certifique-se de executar esses comandos a partir da raiz do repositório do RustDesk, do contrário o aplicativo pode não encontrar os recursos necessários. Note também que outros subcomandos do cargo, como `install` ou `run`, não são suportados atualmente por este método, pois instalariam ou executariam o programa dentro do contêiner em vez de no sistema hospedeiro.
Por favor verifique que está executando estes comandos da raiz do repositório do RustDesk, senão a aplicação pode não encontrar os recursos necessários. Note também que outros subcomandos do cargo como `install` ou `run` não são suportados atualmente via este método, já que eles iriam instalar ou rodar o programa dentro do container ao invés do host.
## Estrutura de Arquivos
## Estrutura de arquivos
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: codec de vídeo, configuração, encapsulador (wrapper) tcp/udp, protobuf, funções de sistema de arquivos para transferência de arquivos e algumas outras funções utilitárias.
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: captura de tela.
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: controle de teclado/mouse específico de cada plataforma.
- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: implementação de copiar e colar arquivos para Windows, Linux e macOS.
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: interface Sciter antiga (descontinuada).
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: serviços de áudio/área de transferência/entrada/vídeo e conexões de rede.
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: inicia uma conexão direta (peer connection).
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Comunica-se com o [rustdesk-server](https://github.com/rustdesk/rustdesk-server), aguarda por conexão remota direta (perfuração de túnel TCP / hole punching) ou retransmitida.
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: código específico de cada plataforma.
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: código Flutter para desktop e dispositivos móveis.
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: JavaScript para o cliente web do Flutter.
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: codec de vídeo, configurações, wrapper de tcp/udp, protobuf, funções de sistema de arquivos para transferência de arquivos, e outras funções utilitárias
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: captura de tela
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: controle de teclado/mouse específico a cada plataforma
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: serviços de áudio/área de transferência/entrada/vídeo, e conexões de rede
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: iniciar uma conexão "peer to peer"
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Comunicação com [rustdesk-server](https://github.com/rustdesk/rustdesk-server), aguardar pela conexão remota direta (TCP hole punching) ou conexão indireta (relayed)
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: código específico a cada plataforma
## Capturas de Tela
> [!Cuidadob]
> **Aviso de uso indevido:** <br>
> Os desenvolvedores do RustDesk não aprovam nem apoiam qualquer uso antiético ou ilegal deste software. O uso indevido, como acesso não autorizado, controle ou invasão de privacidade, é estritamente contra nossas diretrizes. Os autores não são responsáveis por qualquer uso indevido da aplicação.
![Gerenciador de Conexões](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651)
## Screenshots
![Conectado a um PC Windows](https://github.com/rustdesk/rustdesk/assets/28412477/9baa91e9-3362-4d06-aa1a-7518edcbd7ea)
![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png)
![Transferência de Arquivos](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad)
![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png)
![Tunelamento TCP](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5)
![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png)
![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png)

View File

@@ -33,4 +33,4 @@ if [ -z $release ]; then
fi
set -f
#shellcheck disable=2086
VCPKG_ROOT=/vcpkg cargo build --locked $argv
VCPKG_ROOT=/vcpkg cargo build $argv

View File

@@ -460,7 +460,6 @@ build)
--target "${RUST_TARGET}" \
--bindgen \
build \
--locked \
--release \
--features "${RUSTDESK_FEATURES}"

View File

@@ -1,2 +1,2 @@
#!/usr/bin/env bash
cargo build --locked --features flutter,hwcodec --release --target aarch64-apple-ios --lib
cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib

View File

@@ -1,2 +1,2 @@
#!/usr/bin/env bash
cargo build --locked --features flutter --release --target x86_64-apple-ios --lib
cargo build --features flutter --release --target x86_64-apple-ios --lib

View File

@@ -716,17 +716,6 @@ closeConnection({String? id}) {
stateGlobal.isInMainPage = true;
} else {
final controller = Get.find<DesktopTabController>();
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);
}
}
@@ -4190,7 +4179,8 @@ Widget? buildAvatarWidget({
width: size,
height: size,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => fallback ?? SizedBox.shrink(),
errorBuilder: (_, __, ___) =>
fallback ?? SizedBox.shrink(),
),
);
}

View File

@@ -0,0 +1,65 @@
// flutter/lib/common/widgets/keyboard_shortcuts/display.dart
import 'dart:convert';
import 'package:flutter/foundation.dart';
import '../../../consts.dart';
import '../../../models/platform_model.dart';
/// Read the bindings JSON and produce a human-readable shortcut string for
/// `actionId`, formatted for the current OS. Returns null if unbound.
class ShortcutDisplay {
static String? formatFor(String actionId) {
final raw = bind.mainGetLocalOption(key: kShortcutLocalConfigKey);
if (raw.isEmpty) return null;
final Map<String, dynamic> parsed;
try {
parsed = jsonDecode(raw) as Map<String, dynamic>;
} catch (_) {
return null;
}
if (parsed['enabled'] != true) return null;
final list = (parsed['bindings'] as List? ?? []).cast<Map<String, dynamic>>();
final found = list.firstWhere(
(b) => b['action'] == actionId,
orElse: () => {},
);
if (found.isEmpty) return null;
// Guard against a hand-edited / corrupt config where `key` is missing or
// not a string — silently treat the binding as unbound rather than
// crashing the toolbar render.
final keyValue = found['key'];
if (keyValue is! String) return null;
final isMac = defaultTargetPlatform == TargetPlatform.macOS ||
defaultTargetPlatform == TargetPlatform.iOS;
// `mods` similarly may be malformed; treat a non-list as no modifiers.
final modsRaw = found['mods'];
final mods = modsRaw is List
? modsRaw.whereType<String>().toList()
: const <String>[];
final parts = <String>[];
for (final m in ['primary', 'alt', 'shift']) {
if (!mods.contains(m)) continue;
switch (m) {
case 'primary': parts.add(isMac ? '' : 'Ctrl'); break;
case 'alt': parts.add(isMac ? '' : 'Alt'); break;
case 'shift': parts.add(isMac ? '' : 'Shift'); break;
}
}
parts.add(_keyDisplay(keyValue, isMac));
return isMac ? parts.join('') : parts.join('+');
}
static String _keyDisplay(String key, bool isMac) {
switch (key) {
case 'delete': return isMac ? '' : 'Del';
case 'enter': return isMac ? '' : 'Enter';
case 'arrow_left': return '';
case 'arrow_right':return '';
case 'arrow_up': return '';
case 'arrow_down': return '';
}
if (key.startsWith('digit')) return key.substring(5);
return key.toUpperCase();
}
}

View File

@@ -0,0 +1,490 @@
// flutter/lib/common/widgets/keyboard_shortcuts/page_body.dart
//
// Shared body widget for the Keyboard Shortcuts configuration page. Both the
// desktop (`desktop/pages/desktop_keyboard_shortcuts_page.dart`) and mobile
// (`mobile/pages/mobile_keyboard_shortcuts_page.dart`) pages render this
// widget inside their own platform-styled Scaffold + AppBar shell.
//
// The body owns:
// * the top-level enable/disable toggle (mirrors the General-tab toggle —
// same JSON key, same semantics);
// * a grouped list of actions, each with its current binding plus
// edit / clear icons;
// * the JSON read/write helpers under [kShortcutLocalConfigKey] in the
// canonical {enabled, bindings:[{action,mods,key}]} shape;
// * the recording-dialog round-trip and conflict-replace bookkeeping;
// * "Reset to defaults" (called from the platform AppBar).
//
// Platform shells supply only:
// * the AppBar (with a "Reset to defaults" action that calls
// [KeyboardShortcutsPageBodyState.resetToDefaultsWithConfirm]);
// * surrounding padding / list-tile vs. dense-row visuals via the
// [compact] flag.
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import '../../../common.dart';
import '../../../consts.dart';
import '../../../models/platform_model.dart';
import '../../../models/shortcut_model.dart';
import 'recording_dialog.dart';
/// One configurable action — id + i18n key for its label.
class KeyboardShortcutActionEntry {
final String id;
final String labelKey;
const KeyboardShortcutActionEntry(this.id, this.labelKey);
}
/// A named group of actions (e.g. "Session Control").
class KeyboardShortcutActionGroup {
final String titleKey;
final List<KeyboardShortcutActionEntry> actions;
const KeyboardShortcutActionGroup(this.titleKey, this.actions);
}
/// Canonical action group definitions used by both the desktop and mobile
/// configuration pages. The order of groups and entries here is the order
/// the user sees in the UI. (Not `const` because the per-tab ids come from
/// the `kShortcutActionSwitchTab(n)` helper in `consts.dart`.)
final List<KeyboardShortcutActionGroup> kKeyboardShortcutActionGroups = [
KeyboardShortcutActionGroup('Session Control', [
KeyboardShortcutActionEntry(
kShortcutActionSendCtrlAltDel, 'Insert Ctrl + Alt + Del'),
KeyboardShortcutActionEntry(kShortcutActionInsertLock, 'Insert Lock'),
KeyboardShortcutActionEntry(kShortcutActionRefresh, 'Refresh'),
KeyboardShortcutActionEntry(kShortcutActionSwitchSides, 'Switch Sides'),
KeyboardShortcutActionEntry(
kShortcutActionToggleRecording, 'Toggle Recording'),
KeyboardShortcutActionEntry(
kShortcutActionToggleBlockInput, 'Toggle Block User Input'),
]),
KeyboardShortcutActionGroup('Display', [
KeyboardShortcutActionEntry(
kShortcutActionToggleFullscreen, 'Toggle Fullscreen'),
KeyboardShortcutActionEntry(
kShortcutActionSwitchDisplayNext, 'Switch to next display'),
KeyboardShortcutActionEntry(
kShortcutActionSwitchDisplayPrev, 'Switch to previous display'),
KeyboardShortcutActionEntry(kShortcutActionViewMode1to1, 'View Mode 1:1'),
KeyboardShortcutActionEntry(
kShortcutActionViewModeShrink, 'View Mode Shrink'),
KeyboardShortcutActionEntry(
kShortcutActionViewModeStretch, 'View Mode Stretch'),
]),
KeyboardShortcutActionGroup('Other', [
KeyboardShortcutActionEntry(kShortcutActionScreenshot, 'Take Screenshot'),
KeyboardShortcutActionEntry(kShortcutActionToggleAudio, 'Toggle Audio'),
KeyboardShortcutActionEntry(
kShortcutActionTogglePrivacyMode, 'Toggle Privacy Mode'),
for (var n = 1; n <= 9; n++)
KeyboardShortcutActionEntry(
kShortcutActionSwitchTab(n), 'Switch Tab $n'),
]),
];
/// The shared body widget. Render this inside a platform-styled Scaffold.
///
/// [compact] toggles the desktop dense-row layout (`true`) versus the mobile
/// touch-friendly ListTile layout (`false`).
///
/// [editButtonHint] is shown as the tooltip on the Edit icon. Mobile shells
/// use this to clarify that recording requires a physical keyboard.
///
/// [headerBanner] is an optional widget rendered above the toggle. Mobile
/// uses this to show the "Recording requires a physical keyboard" hint.
class KeyboardShortcutsPageBody extends StatefulWidget {
final bool compact;
final String? editButtonHint;
final Widget? headerBanner;
const KeyboardShortcutsPageBody({
Key? key,
this.compact = true,
this.editButtonHint,
this.headerBanner,
}) : super(key: key);
@override
State<KeyboardShortcutsPageBody> createState() =>
KeyboardShortcutsPageBodyState();
}
/// Public state so platform shells can call [resetToDefaultsWithConfirm] from
/// their AppBar action.
class KeyboardShortcutsPageBodyState extends State<KeyboardShortcutsPageBody> {
// ----- Persistence helpers -----
Map<String, dynamic> _readJson() {
final raw = bind.mainGetLocalOption(key: kShortcutLocalConfigKey);
if (raw.isEmpty) return {'enabled': false, 'bindings': <dynamic>[]};
try {
final parsed = jsonDecode(raw) as Map<String, dynamic>;
parsed['bindings'] ??= <dynamic>[];
parsed['enabled'] ??= false;
return parsed;
} catch (_) {
return {'enabled': false, 'bindings': <dynamic>[]};
}
}
Future<void> _writeJson(Map<String, dynamic> json) async {
await bind.mainSetLocalOption(
key: kShortcutLocalConfigKey, value: jsonEncode(json));
// Refresh the matcher cache so writes take effect immediately. On native
// this hits the Rust matcher; on Web the bridge forwards to the JS-side
// matcher in flutter/web/js/.
bind.mainReloadKeyboardShortcuts();
if (mounted) setState(() {});
}
/// Replace the bindings entry for [actionId] with [binding]. If [binding]
/// is null, removes the existing entry. If the user is replacing a
/// conflicting binding, [clearActionId] points at the action whose
/// (now-stale) binding should be removed in the same write.
Future<void> _setBinding(
String actionId, {
Map<String, dynamic>? binding,
String? clearActionId,
}) async {
final json = _readJson();
final list = ((json['bindings'] as List?) ?? <dynamic>[])
.cast<Map<String, dynamic>>()
.toList();
list.removeWhere((b) {
final a = b['action'];
return a == actionId || (clearActionId != null && a == clearActionId);
});
if (binding != null) {
list.add(binding);
}
json['bindings'] = list;
await _writeJson(json);
}
Future<void> _setEnabled(bool v) async {
final json = _readJson();
json['enabled'] = v;
// First-time enable: seed defaults if the user has never bound anything.
final list = (json['bindings'] as List?) ?? const [];
if (v && list.isEmpty) {
json['bindings'] = jsonDecode(bind.mainGetDefaultKeyboardShortcuts());
}
await _writeJson(json);
}
Future<void> _resetToDefaults() async {
final json = _readJson();
json['bindings'] = jsonDecode(bind.mainGetDefaultKeyboardShortcuts());
await _writeJson(json);
}
String _labelFor(String actionId) {
for (final g in kKeyboardShortcutActionGroups) {
for (final a in g.actions) {
if (a.id == actionId) return translate(a.labelKey);
}
}
return actionId;
}
// ----- UI handlers -----
Future<void> _onEdit(KeyboardShortcutActionEntry entry) async {
final json = _readJson();
final bindings = ((json['bindings'] as List?) ?? <dynamic>[])
.cast<Map<String, dynamic>>();
final result = await showRecordingDialog(
context: context,
actionId: entry.id,
actionLabel: translate(entry.labelKey),
existingBindings: bindings,
actionLabelLookup: _labelFor,
);
if (result == null) return;
await _setBinding(
entry.id,
binding: result.binding,
clearActionId: result.clearActionId,
);
}
Future<void> _onClear(KeyboardShortcutActionEntry entry) async {
await _setBinding(entry.id, binding: null);
}
/// Public — invoked from the platform AppBar action.
Future<void> resetToDefaultsWithConfirm() async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: Text(translate('Reset to defaults')),
content: Text(translate('shortcut-reset-confirm-tip')),
actions: [
dialogButton('Cancel',
onPressed: () => Navigator.of(ctx).pop(false),
isOutline: true),
dialogButton('OK', onPressed: () => Navigator.of(ctx).pop(true)),
],
),
);
if (confirmed == true) {
await _resetToDefaults();
}
}
// ----- Build -----
@override
Widget build(BuildContext context) {
final enabled = ShortcutModel.isEnabled();
final theme = Theme.of(context);
return ListView(
padding: const EdgeInsets.all(16),
children: [
if (widget.headerBanner != null) ...[
widget.headerBanner!,
const SizedBox(height: 12),
],
// Top toggle — mirrors the General-tab _OptionCheckBox semantics.
Row(
children: [
Checkbox(
value: enabled,
onChanged: (v) async {
if (v == null) return;
await _setEnabled(v);
},
),
const SizedBox(width: 4),
Expanded(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => _setEnabled(!enabled),
child: Text(
translate('Enable keyboard shortcuts in remote session'),
),
),
),
],
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
translate('shortcut-page-description'),
style: TextStyle(color: theme.hintColor),
),
),
const SizedBox(height: 16),
// Disabled visual state when toggle is off — but still scrollable.
Opacity(
opacity: enabled ? 1.0 : 0.5,
child: AbsorbPointer(
absorbing: !enabled,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (final group in kKeyboardShortcutActionGroups)
_buildGroup(context, group),
],
),
),
),
],
);
}
Widget _buildGroup(BuildContext context, KeyboardShortcutActionGroup group) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
children: [
Text(
translate(group.titleKey),
style: TextStyle(
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.primary,
),
),
const SizedBox(width: 8),
const Expanded(
child: Divider(thickness: 1),
),
],
),
),
const SizedBox(height: 4),
for (final action in group.actions)
widget.compact
? _buildCompactRow(context, action)
: _buildTouchRow(context, action),
],
);
}
/// Desktop dense row: label | shortcut | edit | clear, all in one Row.
Widget _buildCompactRow(
BuildContext context, KeyboardShortcutActionEntry entry) {
final shortcut = ShortcutDisplayForActionId.format(entry.id);
final hasBinding = shortcut != null;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: Row(
children: [
Expanded(
flex: 5,
child: Text(translate(entry.labelKey)),
),
Expanded(
flex: 4,
child: Text(
shortcut ?? '',
style: TextStyle(
fontFamily: defaultTargetPlatform == TargetPlatform.windows
? 'Consolas'
: 'monospace',
color: hasBinding ? null : Theme.of(context).hintColor,
),
),
),
IconButton(
tooltip: widget.editButtonHint ?? translate('Edit'),
onPressed: () => _onEdit(entry),
icon: const Icon(Icons.edit_outlined, size: 18),
),
SizedBox(
width: 40,
child: hasBinding
? IconButton(
tooltip: translate('Clear'),
onPressed: () => _onClear(entry),
icon: const Icon(Icons.close, size: 18),
)
: const SizedBox.shrink(),
),
],
),
);
}
/// Mobile touch row: ListTile with title + subtitle + trailing icons.
Widget _buildTouchRow(
BuildContext context, KeyboardShortcutActionEntry entry) {
final shortcut = ShortcutDisplayForActionId.format(entry.id);
final hasBinding = shortcut != null;
return ListTile(
dense: false,
contentPadding: const EdgeInsets.symmetric(horizontal: 8),
title: Text(translate(entry.labelKey)),
subtitle: Text(
shortcut ?? '',
style: TextStyle(
fontFamily: defaultTargetPlatform == TargetPlatform.windows
? 'Consolas'
: 'monospace',
color: hasBinding ? null : Theme.of(context).hintColor,
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
tooltip: widget.editButtonHint ?? translate('Edit'),
onPressed: () => _onEdit(entry),
icon: const Icon(Icons.edit_outlined),
),
if (hasBinding)
IconButton(
tooltip: translate('Clear'),
onPressed: () => _onClear(entry),
icon: const Icon(Icons.close),
)
else
const SizedBox(width: 48),
],
),
);
}
}
/// Thin wrapper around [ShortcutDisplay.formatFor] that ignores the
/// `enabled` flag so the configuration page can always show the user what
/// they have bound, even when the feature is currently disabled.
class ShortcutDisplayForActionId {
static String? format(String actionId) {
final raw = bind.mainGetLocalOption(key: kShortcutLocalConfigKey);
if (raw.isEmpty) return null;
final Map<String, dynamic> parsed;
try {
parsed = jsonDecode(raw) as Map<String, dynamic>;
} catch (_) {
return null;
}
final list = (parsed['bindings'] as List? ?? const [])
.cast<Map<String, dynamic>>();
final found = list.firstWhere(
(b) => b['action'] == actionId,
orElse: () => {},
);
if (found.isEmpty) return null;
// Guard against a hand-edited / corrupt config where `key` is missing or
// not a string — render the row as unbound instead of crashing the
// settings page.
final keyValue = found['key'];
if (keyValue is! String) return null;
final isMac = defaultTargetPlatform == TargetPlatform.macOS ||
defaultTargetPlatform == TargetPlatform.iOS;
// `mods` similarly may be malformed; treat a non-list as no modifiers.
final modsRaw = found['mods'];
final mods = modsRaw is List
? modsRaw.whereType<String>().toList()
: const <String>[];
final parts = <String>[];
for (final m in ['primary', 'alt', 'shift']) {
if (!mods.contains(m)) continue;
switch (m) {
case 'primary':
parts.add(isMac ? '' : 'Ctrl');
break;
case 'alt':
parts.add(isMac ? '' : 'Alt');
break;
case 'shift':
parts.add(isMac ? '' : 'Shift');
break;
}
}
parts.add(_keyDisplay(keyValue, isMac));
return isMac ? parts.join('') : parts.join('+');
}
static String _keyDisplay(String key, bool isMac) {
switch (key) {
case 'delete':
return isMac ? '' : 'Del';
case 'enter':
return isMac ? '' : 'Enter';
case 'arrow_left':
return '';
case 'arrow_right':
return '';
case 'arrow_up':
return '';
case 'arrow_down':
return '';
}
if (key.startsWith('digit')) return key.substring(5);
return key.toUpperCase();
}
}

View File

@@ -0,0 +1,371 @@
// flutter/lib/common/widgets/keyboard_shortcuts/recording_dialog.dart
//
// Modal dialog used by the Keyboard Shortcuts settings page to capture a new
// key combination for a given action. The dialog listens for KeyDown events,
// extracts the modifier set + non-modifier key, validates against the
// "must include Ctrl+Alt+Shift (Cmd+Option+Shift on macOS)" rule, and reports
// any conflict with another already-bound action.
//
// On Save, returns the new binding map ({action, mods, key}) plus the
// optional id of the action whose binding should be cleared (the conflict
// "Replace" path). On Cancel, returns null.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../common.dart';
/// Result of the recording dialog.
class RecordingResult {
/// The new binding map to write: {action, mods, key}.
final Map<String, dynamic> binding;
/// If the chosen combo conflicted with another action, the user chose
/// "Replace" — the caller must clear this action's binding before writing
/// the new one.
final String? clearActionId;
RecordingResult(this.binding, this.clearActionId);
}
/// Show the recording dialog.
///
/// [actionId] is the action being edited (used for the title and to detect
/// "binding to itself" — that's not a conflict).
/// [actionLabel] is the translated, user-facing action name.
/// [existingBindings] is the current bindings list (used for conflict detection).
/// [actionLabelLookup] resolves an actionId to its translated label, used in
/// the conflict warning.
Future<RecordingResult?> showRecordingDialog({
required BuildContext context,
required String actionId,
required String actionLabel,
required List<Map<String, dynamic>> existingBindings,
required String Function(String) actionLabelLookup,
}) {
return showDialog<RecordingResult>(
context: context,
barrierDismissible: false,
builder: (ctx) => _RecordingDialog(
actionId: actionId,
actionLabel: actionLabel,
existingBindings: existingBindings,
actionLabelLookup: actionLabelLookup,
),
);
}
class _RecordingDialog extends StatefulWidget {
final String actionId;
final String actionLabel;
final List<Map<String, dynamic>> existingBindings;
final String Function(String) actionLabelLookup;
const _RecordingDialog({
required this.actionId,
required this.actionLabel,
required this.existingBindings,
required this.actionLabelLookup,
});
@override
State<_RecordingDialog> createState() => _RecordingDialogState();
}
class _RecordingDialogState extends State<_RecordingDialog> {
final FocusNode _focusNode = FocusNode();
// Captured combo. null until the user presses something with a non-modifier.
Set<String> _mods = {};
String? _key;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_focusNode.requestFocus();
});
}
@override
void dispose() {
_focusNode.dispose();
super.dispose();
}
bool get _isMac =>
defaultTargetPlatform == TargetPlatform.macOS ||
defaultTargetPlatform == TargetPlatform.iOS;
/// True when the captured combo includes the required Ctrl+Alt+Shift
/// (Cmd+Option+Shift on macOS) prefix and a non-modifier key.
bool get _hasRequiredPrefix =>
_mods.contains('primary') &&
_mods.contains('alt') &&
_mods.contains('shift');
/// Return the actionId that this combo currently conflicts with, or null.
/// The action being edited is not a conflict with itself.
String? get _conflictActionId {
if (_key == null || !_hasRequiredPrefix) return null;
for (final b in widget.existingBindings) {
final otherAction = b['action'] as String?;
if (otherAction == null || otherAction == widget.actionId) continue;
final otherKey = b['key'] as String?;
final otherMods =
((b['mods'] as List?) ?? const []).cast<String>().toSet();
if (otherKey == _key &&
otherMods.length == _mods.length &&
otherMods.containsAll(_mods)) {
return otherAction;
}
}
return null;
}
KeyEventResult _onKeyEvent(FocusNode node, KeyEvent event) {
if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.escape) {
Navigator.of(context).pop();
return KeyEventResult.handled;
}
if (event is! KeyDownEvent) return KeyEventResult.handled;
// Ignore modifier-only KeyDowns: don't lock in a partial combo.
final logical = event.logicalKey;
final keyName = _logicalToKeyName(logical);
final mods = <String>{};
if (HardwareKeyboard.instance.isAltPressed) mods.add('alt');
if (HardwareKeyboard.instance.isShiftPressed) mods.add('shift');
final primary = _isMac
? HardwareKeyboard.instance.isMetaPressed
: HardwareKeyboard.instance.isControlPressed;
if (primary) mods.add('primary');
setState(() {
_mods = mods;
// Only lock in the key when it's a non-modifier we recognize.
// Modifier-only KeyDowns (Shift, Ctrl, etc.) leave the captured key
// untouched, so the user can adjust modifiers after the fact.
if (keyName != null) {
_key = keyName;
}
});
return KeyEventResult.handled;
}
void _onSave() {
if (_key == null || !_hasRequiredPrefix) return;
// Sort mods to match the canonical order used by Rust default_bindings:
// primary, alt, shift.
final ordered = <String>[
if (_mods.contains('primary')) 'primary',
if (_mods.contains('alt')) 'alt',
if (_mods.contains('shift')) 'shift',
];
final binding = <String, dynamic>{
'action': widget.actionId,
'mods': ordered,
'key': _key!,
};
Navigator.of(context).pop(RecordingResult(binding, _conflictActionId));
}
String _formatPrefix() {
if (_isMac) return 'Cmd+Option+Shift';
return 'Ctrl+Alt+Shift';
}
String _formatCombo() {
final parts = <String>[];
for (final m in ['primary', 'alt', 'shift']) {
if (!_mods.contains(m)) continue;
switch (m) {
case 'primary':
parts.add(_isMac ? '' : 'Ctrl');
break;
case 'alt':
parts.add(_isMac ? '' : 'Alt');
break;
case 'shift':
parts.add(_isMac ? '' : 'Shift');
break;
}
}
if (_key != null) {
parts.add(_keyDisplay(_key!));
}
if (parts.isEmpty) return translate('shortcut-recording-press-keys-tip');
return _isMac ? parts.join('') : parts.join('+');
}
String _keyDisplay(String key) {
switch (key) {
case 'delete':
return _isMac ? '' : 'Del';
case 'enter':
return _isMac ? '' : 'Enter';
case 'arrow_left':
return '';
case 'arrow_right':
return '';
case 'arrow_up':
return '';
case 'arrow_down':
return '';
}
if (key.startsWith('digit')) return key.substring(5);
return key.toUpperCase();
}
@override
Widget build(BuildContext context) {
final hasKey = _key != null;
final conflictId = _conflictActionId;
final hasConflict = conflictId != null;
final canSave = hasKey && _hasRequiredPrefix;
Widget statusLine;
if (!hasKey) {
statusLine = Text(
translate('shortcut-recording-press-keys-tip'),
style: TextStyle(color: Theme.of(context).hintColor),
);
} else if (!_hasRequiredPrefix) {
statusLine = Row(
children: [
Icon(Icons.close, size: 16, color: Colors.red),
const SizedBox(width: 6),
Flexible(
child: Text(
'${translate('shortcut-must-include-prefix')} ${_formatPrefix()}',
style: const TextStyle(color: Colors.red),
),
),
],
);
} else if (hasConflict) {
final otherLabel = widget.actionLabelLookup(conflictId);
statusLine = Row(
children: [
Icon(Icons.warning_amber_outlined,
size: 16, color: Colors.orange.shade700),
const SizedBox(width: 6),
Flexible(
child: Text(
'${translate('shortcut-already-bound-to')} "$otherLabel"',
style: TextStyle(color: Colors.orange.shade700),
),
),
],
);
} else {
statusLine = Row(
children: [
const Icon(Icons.check, size: 16, color: Colors.green),
const SizedBox(width: 6),
Text(translate('Valid'),
style: const TextStyle(color: Colors.green)),
],
);
}
final saveLabel = hasConflict ? 'Replace' : 'Save';
return AlertDialog(
title: Text(
'${translate('Set Shortcut')}: ${widget.actionLabel}',
),
content: Focus(
focusNode: _focusNode,
autofocus: true,
onKeyEvent: _onKeyEvent,
child: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 380),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(translate('shortcut-recording-instruction')),
const SizedBox(height: 12),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
vertical: 18, horizontal: 12),
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).dividerColor),
borderRadius: BorderRadius.circular(4),
),
child: Text(
_formatCombo(),
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: hasKey
? Theme.of(context).textTheme.titleLarge?.color
: Theme.of(context).hintColor,
),
),
),
const SizedBox(height: 12),
statusLine,
],
),
),
),
actions: [
dialogButton('Cancel',
onPressed: () => Navigator.of(context).pop(),
isOutline: true),
dialogButton(saveLabel, onPressed: canSave ? _onSave : null),
],
);
}
/// Mirror of `event_to_key_name` in `src/keyboard/shortcuts.rs` and
/// `logicalToKeyName` in `flutter/web/js/src/shortcut_matcher.ts` — keep
/// the three in lockstep. Returns null for modifier-only or unsupported keys.
static String? _logicalToKeyName(LogicalKeyboardKey k) {
if (k == LogicalKeyboardKey.delete) return 'delete';
if (k == LogicalKeyboardKey.enter ||
k == LogicalKeyboardKey.numpadEnter) return 'enter';
if (k == LogicalKeyboardKey.arrowLeft) return 'arrow_left';
if (k == LogicalKeyboardKey.arrowRight) return 'arrow_right';
if (k == LogicalKeyboardKey.arrowUp) return 'arrow_up';
if (k == LogicalKeyboardKey.arrowDown) return 'arrow_down';
final letters = <LogicalKeyboardKey, String>{
LogicalKeyboardKey.keyA: 'a', LogicalKeyboardKey.keyB: 'b',
LogicalKeyboardKey.keyC: 'c', LogicalKeyboardKey.keyD: 'd',
LogicalKeyboardKey.keyE: 'e', LogicalKeyboardKey.keyF: 'f',
LogicalKeyboardKey.keyG: 'g', LogicalKeyboardKey.keyH: 'h',
LogicalKeyboardKey.keyI: 'i', LogicalKeyboardKey.keyJ: 'j',
LogicalKeyboardKey.keyK: 'k', LogicalKeyboardKey.keyL: 'l',
LogicalKeyboardKey.keyM: 'm', LogicalKeyboardKey.keyN: 'n',
LogicalKeyboardKey.keyO: 'o', LogicalKeyboardKey.keyP: 'p',
LogicalKeyboardKey.keyQ: 'q', LogicalKeyboardKey.keyR: 'r',
LogicalKeyboardKey.keyS: 's', LogicalKeyboardKey.keyT: 't',
LogicalKeyboardKey.keyU: 'u', LogicalKeyboardKey.keyV: 'v',
LogicalKeyboardKey.keyW: 'w', LogicalKeyboardKey.keyX: 'x',
LogicalKeyboardKey.keyY: 'y', LogicalKeyboardKey.keyZ: 'z',
};
if (letters.containsKey(k)) return letters[k];
final digits = <LogicalKeyboardKey, String>{
LogicalKeyboardKey.digit1: 'digit1',
LogicalKeyboardKey.digit2: 'digit2',
LogicalKeyboardKey.digit3: 'digit3',
LogicalKeyboardKey.digit4: 'digit4',
LogicalKeyboardKey.digit5: 'digit5',
LogicalKeyboardKey.digit6: 'digit6',
LogicalKeyboardKey.digit7: 'digit7',
LogicalKeyboardKey.digit8: 'digit8',
LogicalKeyboardKey.digit9: 'digit9',
};
if (digits.containsKey(k)) return digits[k];
return null;
}
}

View File

@@ -13,81 +13,21 @@ 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';
import 'package:url_launcher/url_launcher.dart';
bool isEditOsPassword = false;
const String kPeerOptionAllowWaylandKeyboard = 'allow-wayland-keyboard';
const String kWaylandKeyboardIssueUrl =
'https://github.com/rustdesk/rustdesk/issues/14586';
final Set<String> _waylandKeyboardPromptSuppressedConnectionIds = <String>{};
Future<bool> openWaylandKeyboardIssueUrl() {
return launchUrl(
Uri.parse(kWaylandKeyboardIssueUrl),
mode: LaunchMode.externalApplication,
);
}
bool isWaylandKeyboardPromptSuppressedForConnection(String connectionId) {
return _waylandKeyboardPromptSuppressedConnectionIds.contains(connectionId);
}
void setWaylandKeyboardPromptSuppressedForConnection(
String connectionId, bool suppressed) {
if (suppressed) {
_waylandKeyboardPromptSuppressedConnectionIds.add(connectionId);
} else {
_waylandKeyboardPromptSuppressedConnectionIds.remove(connectionId);
}
}
void clearWaylandKeyboardPromptSuppressedForConnection(String connectionId) {
_waylandKeyboardPromptSuppressedConnectionIds.remove(connectionId);
}
bool shouldShowWaylandKeyboardPrompt({
required String connectionId,
required bool isWaylandPeer,
required bool allowWaylandKeyboardRemembered,
}) {
return isWaylandPeer &&
!allowWaylandKeyboardRemembered &&
!isWaylandKeyboardPromptSuppressedForConnection(connectionId);
}
Widget waylandKeyboardScopeChip(BuildContext context, String text) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(999),
border: Border.all(color: colorScheme.primary.withOpacity(0.35)),
),
child: Text(
text,
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w600),
),
);
}
// 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;
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) {
@@ -149,179 +89,12 @@ handleOsPasswordAction(
}
}
void showWaylandKeyboardInputWarningDialog(
{required String id,
required String connectionId,
required FFI ffi,
required Future<void> Function() onEnable}) {
bool remember = false;
bool consentInProgress = false;
bool dialogClosed = false;
final dialogFuture = ffi.dialogManager.show((setState, close, context) {
void safeSetState(VoidCallback fn) {
if (dialogClosed) {
return;
}
try {
setState(fn);
} catch (e) {
debugPrint('Ignore setState after dialog disposal: $e');
}
}
void closeDialog() {
if (dialogClosed) {
return;
}
dialogClosed = true;
close();
}
Future<void> enableAndContinue() async {
if (consentInProgress || dialogClosed) {
return;
}
consentInProgress = true;
safeSetState(() {});
try {
await onEnable();
} catch (e, st) {
debugPrint('Failed to enable Wayland keyboard input consent: $e');
debugPrintStack(stackTrace: st);
consentInProgress = false;
safeSetState(() {});
return;
}
ffi.inputModel.keyboardInputAllowed = true;
var rememberPersisted = true;
if (remember) {
try {
await bind.mainSetPeerOption(
id: id,
key: kPeerOptionAllowWaylandKeyboard,
value: bool2option(kPeerOptionAllowWaylandKeyboard, true));
} catch (e) {
rememberPersisted = false;
debugPrint('Failed to persist Wayland keyboard input consent: $e');
}
}
// Always suppress prompt for current connection after explicit consent.
setWaylandKeyboardPromptSuppressedForConnection(connectionId, true);
closeDialog();
if (remember && !rememberPersisted) {
// It's a rare edge case that persisting the user's choice fails.
// Failed to persist the user's choice, but still allow keyboard input for current session.
showToast(translate('Failed'));
}
}
void cancel() {
if (consentInProgress) {
return;
}
closeDialog();
}
return CustomAlertDialog(
title: null,
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
msgboxContent(
'',
'wayland-keyboard-input-disabled-tip',
'wayland-keyboard-input-consent-tip',
),
SizedBox(height: isMobile ? 2 : 6),
if (isMobile) ...[
Text(
translate('wayland-keyboard-input-applies-to-tip'),
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
).marginOnly(bottom: 6),
Wrap(
spacing: 6,
runSpacing: 6,
children: [
waylandKeyboardScopeChip(
context, translate('Send clipboard keystrokes')),
waylandKeyboardScopeChip(
context, translate('wayland-soft-keyboard-input-label')),
],
).marginOnly(bottom: 10),
],
TextButton(
onPressed: consentInProgress
? null
: () async {
try {
final opened = await openWaylandKeyboardIssueUrl();
if (!opened) {
// Opening this optional help link almost never fails in
// normal desktop environments. Keep the result handled
// for review hygiene, but avoid a low-value user toast.
debugPrint('Failed to open Wayland keyboard issue URL');
}
} catch (e) {
debugPrint(
'Failed to open Wayland keyboard issue URL: $e');
}
},
style: TextButton.styleFrom(
foregroundColor: Colors.blue,
padding: EdgeInsets.zero,
minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: Text(
translate('Why this happens'),
style: const TextStyle(decoration: TextDecoration.underline),
),
).marginOnly(bottom: 6),
CheckboxListTile(
value: remember,
dense: true,
contentPadding: EdgeInsets.zero,
controlAffinity: ListTileControlAffinity.leading,
title: Text(translate('remember-wayland-keyboard-choice-tip')),
onChanged: consentInProgress
? null
: (v) {
safeSetState(() => remember = v == true);
},
),
],
),
actions: [
dialogButton(
'Cancel',
onPressed: consentInProgress ? null : cancel,
isOutline: true,
),
dialogButton(
'OK',
onPressed:
consentInProgress ? null : () => unawaited(enableAndContinue()),
),
],
onCancel: consentInProgress ? null : cancel,
onSubmit: consentInProgress ? null : () => unawaited(enableAndContinue()),
);
}, clickMaskDismiss: false, backDismiss: false);
unawaited(dialogFuture.whenComplete(() => dialogClosed = true));
}
List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
final ffiModel = ffi.ffiModel;
final pi = ffiModel.pi;
final perms = ffiModel.permissions;
final sessionId = ffi.sessionId;
final isDefaultConn = ffi.connType == ConnType.defaultConn;
final isWaylandPeer = pi.platform == kPeerPlatformLinux && pi.isWayland;
List<TTextMenu> v = [];
// elevation
@@ -371,60 +144,11 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
v.add(TTextMenu(
child: Text(translate('Send clipboard keystrokes')),
onPressed: () async {
Future<void> sendClipboardKeystrokes() async {
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
if (data != null && data.text != null) {
bind.sessionInputString(
sessionId: sessionId, value: data.text ?? "");
}
ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain);
if (data != null && data.text != null) {
bind.sessionInputString(
sessionId: sessionId, value: data.text ?? "");
}
final allowWaylandKeyboard =
mainGetPeerBoolOptionSync(id, kPeerOptionAllowWaylandKeyboard);
if (shouldShowWaylandKeyboardPrompt(
connectionId: sessionId.toString(),
isWaylandPeer: isWaylandPeer,
allowWaylandKeyboardRemembered: allowWaylandKeyboard,
)) {
ffi.inputModel.keyboardInputAllowed = false;
showWaylandKeyboardInputWarningDialog(
id: id,
connectionId: sessionId.toString(),
ffi: ffi,
onEnable: sendClipboardKeystrokes,
);
return;
}
await sendClipboardKeystrokes();
}));
}
if (isDefaultConn &&
isWaylandPeer &&
(mainGetPeerBoolOptionSync(id, kPeerOptionAllowWaylandKeyboard) ||
isWaylandKeyboardPromptSuppressedForConnection(
sessionId.toString()))) {
v.add(TTextMenu(
child: Text(translate('wayland-keyboard-input-reset-choice-tip')),
onPressed: () async {
var persistedCleared = false;
try {
await bind.mainSetPeerOption(
id: id,
key: kPeerOptionAllowWaylandKeyboard,
value: bool2option(kPeerOptionAllowWaylandKeyboard, false));
persistedCleared = true;
} catch (e) {
debugPrint(
'Failed to clear persisted Wayland keyboard permission: $e');
} finally {
clearWaylandKeyboardPromptSuppressedForConnection(
sessionId.toString());
ffi.inputModel.keyboardInputAllowed = false;
if (isMobile) {
await ffi.invokeMethod("enable_soft_keyboard", false);
}
}
showToast(translate(persistedCleared ? 'Successful' : 'Failed'));
}));
}
// reset canvas
@@ -507,7 +231,8 @@ List<TTextMenu> 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
@@ -528,7 +253,8 @@ List<TTextMenu> 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
@@ -546,7 +272,8 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
sessionId: sessionId,
value: '${blockInput.value ? 'un' : ''}block-input');
blockInput.value = !blockInput.value;
}));
},
actionId: kShortcutActionToggleBlockInput));
}
// switchSides
if (isDefaultConn &&
@@ -558,13 +285,15 @@ List<TTextMenu> 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
@@ -586,7 +315,8 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
)
],
),
onPressed: () => ffi.recordingModel.toggle()));
onPressed: () => ffi.recordingModel.toggle(),
actionId: kShortcutActionToggleRecording));
}
// to-do:
@@ -620,6 +350,7 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
});
}
},
actionId: kShortcutActionScreenshot,
));
}
}
@@ -630,6 +361,13 @@ List<TTextMenu> 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 (final menu in v) {
if (menu.actionId != null && menu.onPressed != null) {
ffi.shortcutModel.register(menu.actionId!, menu.onPressed!);
}
}
return v;
}
@@ -962,9 +700,8 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
child: Text(translate('Lock after session end'))));
}
final privacyModeState = PrivacyModeState.find(id);
if (pi.isSupportMultiDisplay &&
(privacyModeState.isEmpty || allowDisplaySwitchInPrivacyMode(pi)) &&
PrivacyModeState.find(id).isEmpty &&
pi.displaysCount.value > 1 &&
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y') {
final value =
@@ -1038,26 +775,15 @@ List<TToggleMenu> 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<void> Function(SessionID sid, String opt) toggleFunc) {
final enabled = !ffiModel.viewOnly &&
(hasPrivacyModePermission || privacyModeState.isNotEmpty);
final enabled = !ffi.ffiModel.viewOnly;
return TToggleMenu(
value: privacyModeState.isNotEmpty,
onChanged: enabled
? (value) {
if (value == null) return;
if (!allowDisplaySwitchInPrivacyMode(pi) &&
ffiModel.pi.currentDisplay != 0 &&
if (ffiModel.pi.currentDisplay != 0 &&
ffiModel.pi.currentDisplay != kAllDisplayValue) {
msgBox(
sessionId,
@@ -1100,29 +826,18 @@ List<TToggleMenu> toolbarPrivacyMode(
})
];
} else {
final visibleImpls = hasPrivacyModePermission
? privacyModeImpls
: privacyModeImpls.where((e) {
final implKey = (e as List<dynamic>)[0] as String;
return privacyModeState.value == implKey;
}).toList();
return visibleImpls.map((e) {
return privacyModeImpls.map((e) {
final implKey = (e as List<dynamic>)[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: enabled
? (value) {
if (value == null) return;
if (value && !hasPrivacyModePermission) return;
togglePrivacyModeTime = DateTime.now();
bind.sessionTogglePrivacyMode(
sessionId: sessionId, implKey: implKey, on: value);
}
: null);
onChanged: (value) {
if (value == null) return;
togglePrivacyModeTime = DateTime.now();
bind.sessionTogglePrivacyMode(
sessionId: sessionId, implKey: implKey, on: value);
});
}).toList();
}
}

View File

@@ -114,9 +114,6 @@ 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";
@@ -142,10 +139,6 @@ 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";
@@ -693,3 +686,24 @@ extension WindowsTargetExt on int {
}
const kCheckSoftwareUpdateFinish = 'check_software_update_finish';
// Keyboard shortcut Action IDs - must match src/keyboard/shortcuts.rs::action_id.
const kShortcutActionSendCtrlAltDel = 'send_ctrl_alt_del';
const kShortcutActionToggleFullscreen = 'toggle_fullscreen';
const kShortcutActionSwitchDisplayNext = 'switch_display_next';
const kShortcutActionSwitchDisplayPrev = 'switch_display_prev';
const kShortcutActionScreenshot = 'screenshot';
const kShortcutActionInsertLock = 'insert_lock';
const kShortcutActionRefresh = 'refresh';
const kShortcutActionToggleAudio = 'toggle_audio';
const kShortcutActionToggleBlockInput = 'toggle_block_input';
const kShortcutActionToggleRecording = 'toggle_recording';
const kShortcutActionTogglePrivacyMode = 'toggle_privacy_mode';
const kShortcutActionViewMode1to1 = 'view_mode_1_to_1';
const kShortcutActionViewModeShrink = 'view_mode_shrink';
const kShortcutActionViewModeStretch = 'view_mode_stretch';
const kShortcutActionSwitchSides = 'switch_sides';
String kShortcutActionSwitchTab(int n) => 'switch_tab_$n';
const kShortcutLocalConfigKey = 'keyboard-shortcuts';
const kShortcutEventName = 'shortcut_triggered';

View File

@@ -0,0 +1,58 @@
// flutter/lib/desktop/pages/desktop_keyboard_shortcuts_page.dart
//
// Desktop shell for the Keyboard Shortcuts configuration page. Users land
// here from the General settings tab. The page exposes:
// * A top-level enable/disable toggle (mirrors the General-tab toggle —
// same JSON key, same semantics).
// * A grouped, scrollable list of actions, each with a current binding and
// edit / clear icons.
// * An AppBar "Reset to defaults" action with a confirmation dialog.
//
// All edits write back to LocalConfig under [kShortcutLocalConfigKey] in the
// canonical {enabled, bindings:[{action,mods,key}]} shape that the Rust and
// Web matchers consume.
//
// The body — group definitions, JSON I/O, conflict-replace flow,
// recording-dialog round-trip — lives in
// `common/widgets/keyboard_shortcuts/page_body.dart` and is shared with the
// mobile shell at `mobile/pages/mobile_keyboard_shortcuts_page.dart`.
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../common.dart';
import '../../common/widgets/keyboard_shortcuts/page_body.dart';
class DesktopKeyboardShortcutsPage extends StatefulWidget {
const DesktopKeyboardShortcutsPage({Key? key}) : super(key: key);
@override
State<DesktopKeyboardShortcutsPage> createState() =>
_DesktopKeyboardShortcutsPageState();
}
class _DesktopKeyboardShortcutsPageState
extends State<DesktopKeyboardShortcutsPage> {
final GlobalKey<KeyboardShortcutsPageBodyState> _bodyKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(translate('Keyboard Shortcuts')),
actions: [
TextButton.icon(
onPressed: () =>
_bodyKey.currentState?.resetToDefaultsWithConfirm(),
icon: const Icon(Icons.restore),
label: Text(translate('Reset to defaults')),
).marginOnly(right: 12),
],
),
body: KeyboardShortcutsPageBody(
key: _bodyKey,
compact: true,
),
);
}
}

View File

@@ -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,57 @@ 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`) is the single
// source of truth — it embeds an `enabled` boolean alongside the bindings
// list. We mutate the JSON in place via _OptionCheckBox's optGetter /
// optSetter hooks rather than introducing a parallel boolean key, so the
// Rust matcher and the Web matcher both read the same flag without drift.
return _Card(title: 'Keyboard Shortcuts', children: [
_OptionCheckBox(
context,
'Enable keyboard shortcuts in remote session',
kShortcutLocalConfigKey,
isServer: false,
optGetter: ShortcutModel.isEnabled,
optSetter: (k, v) async {
final raw = bind.mainGetLocalOption(key: k);
Map<String, dynamic> parsed = {};
if (raw.isNotEmpty) {
try {
parsed = jsonDecode(raw) as Map<String, dynamic>;
} catch (_) {
parsed = {};
}
}
parsed['enabled'] = v;
parsed['bindings'] ??= <dynamic>[];
// Seed defaults the first time the user enables shortcuts so the
// common combos (Ctrl+Alt+Shift+Enter for fullscreen, etc.) work
// out of the box. Mirrors the same logic on the dedicated config
// page.
final list = (parsed['bindings'] as List?) ?? const [];
if (v && list.isEmpty) {
parsed['bindings'] =
jsonDecode(bind.mainGetDefaultKeyboardShortcuts());
}
await bind.mainSetLocalOption(key: k, value: jsonEncode(parsed));
// Refresh the matcher cache so the new flag / bindings take effect
// immediately. On native this hits the Rust matcher; on Web the
// bridge forwards to the JS-side matcher in flutter/web/js/.
bind.mainReloadKeyboardShortcuts();
},
),
_ShortcutsConfigureRow(),
]);
}
Widget theme() {
final current = MyTheme.getThemeModePreference().toShortString();
onChanged(String value) async {
@@ -488,16 +536,6 @@ 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()) ...[
@@ -1072,10 +1110,6 @@ 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),
@@ -2960,6 +2994,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

View File

@@ -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';
@@ -101,9 +102,6 @@ class _RemotePageState extends State<RemotePage>
Function(bool)? _onEnterOrLeaveImage4Toolbar;
late FFI _ffi;
Worker? _waylandKeyboardModeWorker;
bool _waylandKeyboardModeNormalized = false;
bool _waylandKeyboardModeNormalizing = false;
SessionID get sessionId => _ffi.sessionId;
@@ -129,6 +127,19 @@ class _RemotePageState extends State<RemotePage>
_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);
}
});
_ffi.canvasModel.initializeEdgeScrollFallback(this);
_ffi.start(
@@ -181,48 +192,6 @@ class _RemotePageState extends State<RemotePage>
// Register callback to cancel debounce timer when relative mouse mode is disabled
_ffi.inputModel.onRelativeMouseModeDisabled =
_cancelPointerLockCenterDebounceTimer;
_waylandKeyboardModeWorker = ever(_ffi.ffiModel.pi.isSet, (bool isSet) {
if (isSet) {
unawaited(_normalizeWaylandKeyboardModeIfNeeded());
}
});
if (_ffi.ffiModel.pi.isSet.value) {
unawaited(_normalizeWaylandKeyboardModeIfNeeded());
}
}
Future<void> _normalizeWaylandKeyboardModeIfNeeded() async {
if (!mounted ||
_waylandKeyboardModeNormalized ||
_waylandKeyboardModeNormalizing) {
return;
}
_waylandKeyboardModeNormalizing = true;
try {
final pi = _ffi.ffiModel.pi;
if (pi.platform != kPeerPlatformLinux || !pi.isWayland) return;
final mapSupported = bind.sessionIsKeyboardModeSupported(
sessionId: sessionId, mode: kKeyMapMode);
if (!mapSupported) return;
final current = await bind.sessionGetKeyboardMode(sessionId: sessionId);
if (!mounted) return;
if (current == kKeyMapMode) {
_waylandKeyboardModeNormalized = true;
return;
}
await bind.sessionSetKeyboardMode(
sessionId: sessionId, value: kKeyMapMode);
if (!mounted) return;
await _ffi.inputModel.updateKeyboardMode();
if (!mounted) return;
_waylandKeyboardModeNormalized = true;
} catch (e, st) {
debugPrint('Failed to normalize Wayland keyboard mode: $e');
debugPrintStack(stackTrace: st);
} finally {
_waylandKeyboardModeNormalizing = false;
}
}
/// Cancel the pointer lock center debounce timer
@@ -363,7 +332,6 @@ class _RemotePageState extends State<RemotePage>
_pointerLockCenterDebounceTimer?.cancel();
_pointerLockCenterDebounceTimer = null;
_waylandKeyboardModeWorker?.dispose();
// Clear callback reference to prevent memory leaks and stale references
_ffi.inputModel.onRelativeMouseModeDisabled = null;
// Relative mouse mode cleanup is centralized in FFI.close(closeSession: ...).
@@ -377,9 +345,6 @@ class _RemotePageState extends State<RemotePage>
_ffi.imageModel.disposeImage();
_ffi.cursorModel.disposeImages();
_rawKeyFocusNode.dispose();
if (closeSession) {
clearWaylandKeyboardPromptSuppressedForConnection(sessionId.toString());
}
await _ffi.close(closeSession: closeSession);
_timer?.cancel();
_ffi.dialogManager.dismissAll();

View File

@@ -610,24 +610,19 @@ 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,
{required bool canModify}) {
Function(bool)? onTap, String tooltipText) {
return Tooltip(
message: "$tooltipText: ${enabled ? "ON" : "OFF"}",
waitDuration: Duration.zero,
child: Container(
decoration: BoxDecoration(
color: enabled
? (canModify ? MyTheme.accent : MyTheme.accent.withOpacity(0.6))
: Colors.grey[700],
color: enabled ? MyTheme.accent : Colors.grey[700],
borderRadius: BorderRadius.circular(10.0),
),
padding: EdgeInsets.all(8.0),
child: InkWell(
onTap: canModify
? () =>
checkClickTime(widget.client.id, () => onTap?.call(!enabled))
: null,
onTap: () =>
checkClickTime(widget.client.id, () => onTap?.call(!enabled)),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
@@ -648,9 +643,6 @@ 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,
@@ -697,7 +689,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
});
},
translate('Enable audio'),
canModify: canModifyPermission,
),
buildPermissionIcon(
client.recording,
@@ -712,7 +703,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
});
},
translate('Enable recording session'),
canModify: canModifyPermission,
),
]
: [
@@ -729,7 +719,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
});
},
translate('Enable keyboard/mouse'),
canModify: canModifyPermission,
),
buildPermissionIcon(
client.clipboard,
@@ -744,7 +733,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
});
},
translate('Enable clipboard'),
canModify: canModifyPermission,
),
buildPermissionIcon(
client.audio,
@@ -759,7 +747,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
});
},
translate('Enable audio'),
canModify: canModifyPermission,
),
buildPermissionIcon(
client.file,
@@ -774,7 +761,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
});
},
translate('Enable file copy and paste'),
canModify: canModifyPermission,
),
buildPermissionIcon(
client.restart,
@@ -789,7 +775,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
});
},
translate('Enable remote restart'),
canModify: canModifyPermission,
),
buildPermissionIcon(
client.recording,
@@ -804,7 +789,6 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> {
});
},
translate('Enable recording session'),
canModify: canModifyPermission,
),
// only windows support block input
if (isWindows)
@@ -821,23 +805,6 @@ 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,
)
],
),

View File

@@ -27,7 +27,6 @@ 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<State<TerminalPage>?> _lastState = SimpleWrapper(null);
@@ -44,9 +43,6 @@ class TerminalPage extends StatefulWidget {
class _TerminalPageState extends State<TerminalPage>
with AutomaticKeepAliveClientMixin {
static const EdgeInsets _defaultTerminalPadding =
EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0);
late FFI _ffi;
late TerminalModel _terminalModel;
double? _cellHeight;
@@ -159,27 +155,13 @@ class _TerminalPageState extends State<TerminalPage>
// 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) {
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;
if (_cellHeight == null) {
return const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0);
}
final rows = (heightPx / _cellHeight!).floor();
final extraSpace = heightPx - rows * _cellHeight!;
final topBottom = extraSpace / 2.0;
return EdgeInsets.symmetric(
horizontal: _defaultTerminalPadding.horizontal / 2,
vertical: topBottom,
);
return EdgeInsets.symmetric(horizontal: 5.0, vertical: topBottom);
}
@override

View File

@@ -46,7 +46,6 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
.setTitle(getWindowNameWithId(id));
};
tabController.onRemoved = (_, id) => onRemoveId(id);
tabController.onCloseWindow = _closeWindowFromConnection;
final terminalId = params['terminalId'] ?? _nextTerminalId++;
tabController.add(_createTerminalTab(
peerId: params['id'],
@@ -145,8 +144,6 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
_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.
@@ -371,34 +368,8 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
final persistentSessions =
args['persistent_sessions'] as List<dynamic>? ?? [];
final sortedSessions = persistentSessions.whereType<int>().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) {
if (!existingTerminalIds.add(terminalId)) {
continue;
}
_addNewTerminal(peerId, terminalId: terminalId);
_addNewTerminalForCurrentPeer(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.
@@ -575,11 +546,6 @@ class _TerminalTabPageState extends State<TerminalTabPage> {
}
}
Future<void> _closeWindowFromConnection() async {
await _closeAllTabs();
await WindowController.fromWindowId(windowId()).close();
}
int windowId() {
return widget.params["windowId"];
}

File diff suppressed because it is too large Load Diff

View File

@@ -99,7 +99,6 @@ class DesktopTabController {
/// index, key
Function(int, String)? onRemoved;
Function(String)? onSelected;
Future<void> Function()? onCloseWindow;
DesktopTabController(
{required this.tabType, this.onRemoved, this.onSelected});
@@ -593,13 +592,13 @@ class _DesktopTabState extends State<DesktopTab>
}
Widget _buildBar() {
final isIncomingHomePage = bind.isIncomingOnly() && isInHomePage();
return Row(
children: [
Expanded(
child: GestureDetector(
// custom double tap handler
onTap: !isIncomingHomePage && showMaximize
onTap: !(bind.isIncomingOnly() && isInHomePage()) &&
showMaximize
? () {
final current = DateTime.now().millisecondsSinceEpoch;
final elapsed = current - _lastClickTime;
@@ -610,7 +609,7 @@ class _DesktopTabState extends State<DesktopTab>
.then((value) => stateGlobal.setMaximized(value));
}
}
: (isIncomingHomePage ? () {} : null), // Keep tap recognizer for Windows touch.
: null,
onPanStart: (_) => startDragging(isMainWindow),
onPanCancel: () {
// We want to disable dragging of the tab area in the tab bar.

View File

@@ -27,7 +27,6 @@ import 'common.dart';
import 'consts.dart';
import 'mobile/pages/home_page.dart';
import 'mobile/pages/server_page.dart';
import 'mobile/widgets/deploy_dialog.dart';
import 'models/platform_model.dart';
import 'package:flutter_hbb/plugin/handlers.dart'
@@ -576,14 +575,6 @@ _registerEventHandler() {
NativeUiHandler.instance.onEvent(evt);
});
}
if (isAndroid) {
platformFFI.registerEventHandler(
'android_needs_deploy', 'android_needs_deploy', (_) async {
WidgetsBinding.instance.addPostFrameCallback((_) {
showDeployPromptDialog();
});
});
}
}
Widget keyListenerBuilder(BuildContext context, Widget? child) {

View File

@@ -0,0 +1,95 @@
// flutter/lib/mobile/pages/mobile_keyboard_shortcuts_page.dart
//
// Mobile shell for the Keyboard Shortcuts configuration page. Mirrors
// `desktop/pages/desktop_keyboard_shortcuts_page.dart` but with a touch-
// friendly layout (ListTile rows instead of dense rows) and a hint banner
// that explains the recording flow only works with a physical keyboard.
//
// All actual logic — group definitions, JSON I/O, conflict-replace flow,
// recording-dialog round-trip, "Reset to defaults" — lives in the shared
// `common/widgets/keyboard_shortcuts/page_body.dart`. This file only
// supplies the AppBar, the AppBar action, and the platform hint banner.
//
// Mobile keyboard detection limitation: Flutter has no reliable
// "is a physical keyboard attached?" API on iOS or Android. Soft keyboards
// don't generate the `KeyDownEvent`s the recording dialog listens for, so
// in practice the dialog only does anything useful when the user actually
// has a hardware keyboard plugged in (USB / Bluetooth / Smart Connector).
// For V1 we don't try to detect attachment — we just surface the
// requirement as an in-page hint instead of disabling the Edit button.
import 'package:flutter/material.dart';
import '../../common.dart';
import '../../common/widgets/keyboard_shortcuts/page_body.dart';
class MobileKeyboardShortcutsPage extends StatefulWidget {
const MobileKeyboardShortcutsPage({Key? key}) : super(key: key);
@override
State<MobileKeyboardShortcutsPage> createState() =>
_MobileKeyboardShortcutsPageState();
}
class _MobileKeyboardShortcutsPageState
extends State<MobileKeyboardShortcutsPage> {
final GlobalKey<KeyboardShortcutsPageBodyState> _bodyKey = GlobalKey();
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: Text(translate('Keyboard Shortcuts')),
actions: [
IconButton(
tooltip: translate('Reset to defaults'),
onPressed: () =>
_bodyKey.currentState?.resetToDefaultsWithConfirm(),
icon: const Icon(Icons.restore),
),
],
),
body: KeyboardShortcutsPageBody(
key: _bodyKey,
compact: false,
editButtonHint: translate('shortcut-mobile-physical-keyboard-tip'),
headerBanner: _PhysicalKeyboardHintBanner(theme: theme),
),
);
}
}
/// A muted info banner shown above the master toggle on mobile. We can't
/// reliably detect whether a physical keyboard is attached, so instead of
/// disabling the Edit button we surface the requirement up front.
class _PhysicalKeyboardHintBanner extends StatelessWidget {
final ThemeData theme;
const _PhysicalKeyboardHintBanner({required this.theme});
@override
Widget build(BuildContext context) {
final color = theme.colorScheme.primary.withOpacity(0.08);
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(8),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.info_outline,
size: 18, color: theme.colorScheme.primary),
const SizedBox(width: 8),
Expanded(
child: Text(
translate('shortcut-mobile-physical-keyboard-tip'),
style: TextStyle(color: theme.colorScheme.onSurface),
),
),
],
),
);
}
}

View File

@@ -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';
@@ -75,9 +76,6 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
final FocusNode _physicalFocusNode = FocusNode();
var _showEdit = false; // use soft keyboard
Worker? _waylandKeyboardGateWorker;
bool _waylandKeyboardGateInitialized = false;
InputModel get inputModel => gFFI.inputModel;
SessionID get sessionId => gFFI.sessionId;
@@ -122,35 +120,25 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
}
_disableAndroidSoftKeyboard(
isKeyboardVisible: keyboardVisibilityController.isVisible);
});
WidgetsBinding.instance.addObserver(this);
inputModel.keyboardInputAllowed = true;
// Wayland sessions may use clipboard-based text input on the controlled side.
// Require explicit user confirmation before allowing soft-keyboard and
// clipboard-assisted text input. Physical keyboard events are not gated here.
_waylandKeyboardGateWorker = ever(gFFI.ffiModel.pi.isSet, (bool isSet) {
if (isSet) {
_initWaylandKeyboardGateIfNeeded();
// 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);
}
});
if (gFFI.ffiModel.pi.isSet.value) {
_initWaylandKeyboardGateIfNeeded();
}
WidgetsBinding.instance.addObserver(this);
}
@override
Future<void> dispose() async {
WidgetsBinding.instance.removeObserver(this);
// Close the session up-front. `gFFI.close()` below only calls `sessionClose`
// after several awaits (canvas save, image update, the `enable_soft_keyboard`
// platform call), so if the app is backgrounded while this page is disposing,
// dispose can be suspended before reaching it and the connection is never torn
// down. The reconnect then re-attaches to the leaked session and is stuck on
// "Connecting...". Dispatching it here makes teardown happen synchronously on
// pop; the `sessionClose` in `gFFI.close()` becomes a no-op once removed.
unawaited(bind.sessionClose(sessionId: sessionId));
// https://github.com/flutter/flutter/issues/64935
super.dispose();
gFFI.dialogManager.hideMobileActionsOverlay(store: false);
@@ -160,9 +148,6 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
await gFFI.invokeMethod("enable_soft_keyboard", true);
_mobileFocusNode.dispose();
_physicalFocusNode.dispose();
clearWaylandKeyboardPromptSuppressedForConnection(sessionId.toString());
_waylandKeyboardGateWorker?.dispose();
inputModel.keyboardInputAllowed = true;
await gFFI.close();
_timer?.cancel();
_iosKeyboardWorkaroundTimer?.cancel();
@@ -191,40 +176,6 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
gFFI.invokeMethod("try_sync_clipboard");
}
bool _shouldGateKeyboardForWayland() {
if (!(isAndroid || isIOS)) return false;
final pi = gFFI.ffiModel.pi;
return pi.platform == kPeerPlatformLinux && pi.isWayland;
}
void _initWaylandKeyboardGateIfNeeded() {
if (!mounted) return;
if (_waylandKeyboardGateInitialized) return;
if (!_shouldGateKeyboardForWayland()) return;
_waylandKeyboardGateInitialized = true;
final allowWaylandKeyboard =
mainGetPeerBoolOptionSync(widget.id, kPeerOptionAllowWaylandKeyboard);
if (!shouldShowWaylandKeyboardPrompt(
connectionId: sessionId.toString(),
isWaylandPeer: _shouldGateKeyboardForWayland(),
allowWaylandKeyboardRemembered: allowWaylandKeyboard,
)) {
inputModel.keyboardInputAllowed = true;
return;
}
inputModel.keyboardInputAllowed = false;
// Ensure soft keyboard is not active before user confirms.
_showEdit = false;
gFFI.invokeMethod("enable_soft_keyboard", false);
_mobileFocusNode.unfocus();
_physicalFocusNode.requestFocus();
setState(() {});
}
// 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.
@@ -356,7 +307,7 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
content == '【】')) {
// can not only input content[0], because when input ], [ are also auo insert, which cause ] never be input
bind.sessionInputString(sessionId: sessionId, value: content);
_openKeyboardUnlocked();
openKeyboard();
return;
}
bind.sessionInputString(sessionId: sessionId, value: content);
@@ -368,9 +319,6 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
// handle mobile virtual keyboard
void handleSoftKeyboardInput(String newValue) {
if (!inputModel.keyboardInputAllowed) {
return;
}
if (isIOS) {
_handleIOSSoftKeyboardInput(newValue);
} else {
@@ -379,9 +327,6 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
}
void inputChar(String char) {
if (!inputModel.keyboardInputAllowed) {
return;
}
if (char == '\n') {
char = 'VK_RETURN';
} else if (char == ' ') {
@@ -391,29 +336,6 @@ class _RemotePageState extends State<RemotePage> with WidgetsBindingObserver {
}
void openKeyboard() {
final allowWaylandKeyboard =
mainGetPeerBoolOptionSync(widget.id, kPeerOptionAllowWaylandKeyboard);
if (shouldShowWaylandKeyboardPrompt(
connectionId: sessionId.toString(),
isWaylandPeer: _shouldGateKeyboardForWayland(),
allowWaylandKeyboardRemembered: allowWaylandKeyboard,
)) {
inputModel.keyboardInputAllowed = false;
showWaylandKeyboardInputWarningDialog(
id: widget.id,
connectionId: sessionId.toString(),
ffi: gFFI,
onEnable: () async {
_openKeyboardUnlocked();
},
);
return;
}
_openKeyboardUnlocked();
}
void _openKeyboardUnlocked() {
inputModel.keyboardInputAllowed = true;
gFFI.invokeMethod("enable_soft_keyboard", true);
// destroy first, so that our _value trick can work
_value = initText;
@@ -1274,8 +1196,7 @@ void showOptions(
List<TToggleMenu> privacyModeList = [];
// privacy mode
final privacyModeState = PrivacyModeState.find(id);
if ((gFFI.ffiModel.pi.features.privacyMode && gFFI.ffiModel.keyboard) ||
privacyModeState.isNotEmpty) {
if (gFFI.ffiModel.keyboard && gFFI.ffiModel.pi.features.privacyMode) {
privacyModeList = toolbarPrivacyMode(privacyModeState, context, id, gFFI);
if (privacyModeList.length == 1) {
displayToggles.add(privacyModeList[0]);

View File

@@ -583,16 +583,9 @@ class _PermissionCheckerState extends State<PermissionChecker> {
Widget build(BuildContext context) {
final serverModel = Provider.of<ServerModel>(context);
final hasAudioPermission = androidVersion >= 30;
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;
final hideStopService =
isAndroid &&
bind.mainGetBuildinOption(key: kOptionHideStopService) == 'Y';
return PaddingCard(
title: translate("Permissions"),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
@@ -615,21 +608,13 @@ class _PermissionCheckerState extends State<PermissionChecker> {
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,
enabled: !permissionChangeLocked,
),
PermissionRow(translate("Input Control"), serverModel.inputOk,
serverModel.toggleInput),
PermissionRow(translate("Transfer file"), serverModel.fileOk,
serverModel.toggleFile),
hasAudioPermission
? PermissionRow(translate("Audio Capture"), serverModel.audioOk,
serverModel.toggleAudio,
enabled: !permissionChangeLocked)
serverModel.toggleAudio)
: Row(children: [
Icon(Icons.info_outline).marginOnly(right: 15),
Expanded(
@@ -638,25 +623,19 @@ class _PermissionCheckerState extends State<PermissionChecker> {
style: const TextStyle(color: MyTheme.darkGray),
))
]),
PermissionRow(
translate("Enable clipboard"),
serverModel.clipboardOk,
serverModel.toggleClipboard,
enabled: !permissionChangeLocked,
),
PermissionRow(translate("Enable clipboard"), serverModel.clipboardOk,
serverModel.toggleClipboard),
]));
}
}
class PermissionRow extends StatelessWidget {
const PermissionRow(this.name, this.isOk, this.onPressed,
{Key? key, this.enabled = true})
const PermissionRow(this.name, this.isOk, this.onPressed, {Key? key})
: super(key: key);
final String name;
final bool isOk;
final VoidCallback onPressed;
final bool enabled;
@override
Widget build(BuildContext context) {
@@ -665,11 +644,9 @@ class PermissionRow extends StatelessWidget {
contentPadding: EdgeInsets.all(0),
title: Text(name),
value: isOk,
onChanged: enabled
? (bool value) {
onPressed();
}
: null);
onChanged: (bool value) {
onPressed();
});
}
}

View File

@@ -17,9 +17,10 @@ import '../../common/widgets/login.dart';
import '../../consts.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
import '../widgets/deploy_dialog.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 {
@@ -729,13 +730,6 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
onPressed: (context) {
changeSocks5Proxy();
}),
if (isAndroid && !bind.isOutgoingOnly())
SettingsTile(
title: Text(translate('Deploy')),
leading: Icon(Icons.cloud_upload),
onPressed: (context) {
showDeployDialog();
}),
if (!disabledSettings && !_hideNetwork && !_hideWebSocket)
SettingsTile.switchTile(
title: Text(translate('Use WebSocket')),
@@ -827,6 +821,22 @@ class _SettingsState extends State<SettingsPage> 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')),
@@ -1360,3 +1370,4 @@ SettingsTile _getPopupDialogRadioEntry({
),
);
}

View File

@@ -1,114 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../common.dart';
import '../../models/platform_model.dart';
const _deployDialogTag = 'android-deploy-device';
void showDeployPromptDialog() {
gFFI.dialogManager.dismissByTag(_deployDialogTag);
gFFI.dialogManager.show<bool>((setState, close, context) {
submit() => close(true);
return CustomAlertDialog(
title: Text(translate("Deploy")),
content: Text(translate("server_requires_deployment_tip")),
actions: [
dialogButton("Cancel", onPressed: close, isOutline: true),
dialogButton("OK", onPressed: submit),
],
onSubmit: submit,
onCancel: close,
);
}, tag: _deployDialogTag).then((deploy) {
if (deploy == true) {
showDeployDialog();
}
});
}
void showDeployDialog() {
gFFI.dialogManager.dismissByTag(_deployDialogTag);
final tokenController = TextEditingController();
final idController = TextEditingController();
var errorText = "";
var isInProgress = false;
gFFI.dialogManager.show((setState, close, context) {
submit() async {
if (isInProgress) return;
final token = tokenController.text.trim();
if (token.isEmpty) {
setState(() {
errorText = translate("token is required!");
});
return;
}
setState(() {
errorText = "";
isInProgress = true;
});
String res;
try {
res = await bind.mainDeployDevice(
token: token, id: idController.text.trim());
} catch (e) {
setState(() {
errorText = translate(e.toString());
isInProgress = false;
});
return;
}
if (res.isEmpty) {
close();
await gFFI.serverModel.fetchID();
showToast(translate("Successful"));
} else {
setState(() {
errorText = translate(res.toString());
isInProgress = false;
});
}
}
return CustomAlertDialog(
title: Text(translate("Deploy")),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: tokenController,
decoration: InputDecoration(labelText: translate("API Token")),
obscureText: true,
enableSuggestions: false,
autocorrect: false,
autofocus: true,
).workaroundFreezeLinuxMint(),
TextField(
controller: idController,
decoration:
InputDecoration(labelText: translate("Custom ID (optional)")),
).workaroundFreezeLinuxMint(),
if (errorText.isNotEmpty)
Align(
alignment: Alignment.centerLeft,
child: SelectableText(
errorText,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
fontSize: 12,
),
).paddingOnly(top: 8),
),
if (isInProgress) const LinearProgressIndicator().paddingOnly(top: 8),
],
),
actions: [
dialogButton("Cancel",
onPressed: isInProgress ? null : close, isOutline: true),
dialogButton("OK", onPressed: isInProgress ? null : submit),
],
onSubmit: submit,
onCancel: isInProgress ? null : close,
);
}, tag: _deployDialogTag);
}

View File

@@ -391,30 +391,14 @@ class FileController {
await Future.delayed(Duration(milliseconds: 100));
final savedDir = (await bind.sessionGetPeerOption(
final dir = (await bind.sessionGetPeerOption(
sessionId: sessionId, name: isLocal ? "local_dir" : "remote_dir"));
Future<bool> tryOpenReadyDirs() async {
final dirs = <String>{
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();
openDirectory(dir.isEmpty ? options.value.home : dir);
await Future.delayed(Duration(seconds: 1));
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();
if (directory.value.path.isEmpty) {
openDirectory(options.value.home);
}
}
@@ -445,23 +429,19 @@ class FileController {
});
}
Future<bool> 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<void> refresh() async {
await openDirectory(directory.value.path);
}
Future<bool> openDirectory(String path, {bool isBack = false}) async {
if (!isBack && path == ".") {
return await refresh();
Future<void> openDirectory(String path, {bool isBack = false}) async {
if (path == ".") {
refresh();
return;
}
if (!isBack && path == "..") {
return await _goToParentDirectory(isBack: isBack);
if (path == "..") {
goToParentDirectory();
return;
}
return await _openDirectoryPath(path, isBack: isBack);
}
Future<bool> _openDirectoryPath(String path, {bool isBack = false}) async {
if (!isBack) {
pushHistory();
}
@@ -478,10 +458,8 @@ 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;
}
}
@@ -509,22 +487,19 @@ class FileController {
goBack();
return;
}
unawaited(_openDirectoryPath(path, isBack: true).then<void>((_) {}));
openDirectory(path, isBack: true);
}
void goToParentDirectory() {
unawaited(_goToParentDirectory().then<void>((_) {}));
}
Future<bool> _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) {
return await _openDirectoryPath('/', isBack: isBack);
openDirectory('/');
return;
}
return await _openDirectoryPath(parent, isBack: isBack);
openDirectory(parent);
}
// TODO deprecated this

View File

@@ -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;
@@ -474,10 +474,6 @@ class InputModel {
late final SessionID sessionId;
// Local gate for clipboard-assisted input flows on mobile Wayland dialogs.
// It should not block physical keyboard events.
bool keyboardInputAllowed = true;
bool get keyboardPerm => parent.target!.ffiModel.keyboard;
String get id => parent.target?.id ?? '';
String? get peerPlatform => parent.target?.ffiModel.pi.platform;
@@ -703,6 +699,7 @@ class InputModel {
}
}
<<<<<<< HEAD
// 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() {
@@ -830,6 +827,7 @@ class InputModel {
return KeyEventResult.ignored;
}
}
if (isWindows || isLinux) {
// Ignore meta keys. Because flutter window will loose focus if meta key is pressed.
if (e.physicalKey == PhysicalKeyboardKey.metaLeft ||

View File

@@ -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,

View File

@@ -298,7 +298,7 @@ class ServerModel with ChangeNotifier {
}
toggleAudio() async {
if (clients.any((c) => !c.disconnected)) {
if (clients.isNotEmpty) {
await showClientsMayNotBeChangedAlert(parent.target);
}
if (!_audioOk && !await AndroidPermissionManager.check(kRecordAudio)) {
@@ -316,7 +316,7 @@ class ServerModel with ChangeNotifier {
}
toggleFile() async {
if (clients.any((c) => !c.disconnected)) {
if (clients.isNotEmpty) {
await showClientsMayNotBeChangedAlert(parent.target);
}
if (!_fileOk &&
@@ -345,7 +345,7 @@ class ServerModel with ChangeNotifier {
}
toggleInput() async {
if (clients.any((c) => !c.disconnected)) {
if (clients.isNotEmpty) {
await showClientsMayNotBeChangedAlert(parent.target);
}
if (_inputOk) {
@@ -549,19 +549,10 @@ 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 {
final index = _clients.indexWhere((c) => c.id == client.id);
if (index >= 0) {
_clients[index].privacyMode = client.privacyMode;
notifyListeners();
if (_clients.any((c) => c.id == client.id)) {
return;
}
_clients.add(client);
@@ -827,7 +818,6 @@ class Client {
bool restart = false;
bool recording = false;
bool blockInput = false;
bool privacyMode = false;
bool disconnected = false;
bool fromSwitch = false;
bool inVoiceCall = false;
@@ -856,7 +846,6 @@ 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'];
@@ -881,7 +870,6 @@ 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;

View File

@@ -0,0 +1,141 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import '../common.dart';
import '../consts.dart';
import '../desktop/widgets/tabbar_widget.dart' show DesktopTabController;
import '../models/model.dart';
import '../models/platform_model.dart';
import '../models/state_model.dart';
/// Per-session shortcut dispatcher. Attached to FFI when a session is created.
///
/// The Rust matcher (src/keyboard/shortcuts.rs) emits `shortcut_triggered`
/// session events containing the matched `action` id. The session event
/// listener in [FfiModel.startEventListener] forwards those to this model
/// via [onTriggered], which runs whatever callback the toolbar / menu
/// builders previously registered for that action id.
class ShortcutModel {
final WeakReference<FFI> parent;
final Map<String, VoidCallback> _callbacks = {};
ShortcutModel(this.parent);
/// Called by toolbar / menu builders to register what to do when the
/// matched shortcut fires.
void register(String actionId, VoidCallback callback) {
_callbacks[actionId] = callback;
}
void unregister(String actionId) {
_callbacks.remove(actionId);
}
/// Called by the session event listener when a `shortcut_triggered` event
/// arrives for this session.
void onTriggered(String actionId) {
final cb = _callbacks[actionId];
if (cb != null) {
cb();
} else {
debugPrint('shortcut_triggered: no handler for $actionId');
}
}
/// Read the bindings JSON from LocalConfig.
static List<Map<String, dynamic>> readBindings() {
final raw = bind.mainGetLocalOption(key: kShortcutLocalConfigKey);
if (raw.isEmpty) return [];
try {
final parsed = jsonDecode(raw) as Map<String, dynamic>;
final list = (parsed['bindings'] as List?) ?? [];
return list.cast<Map<String, dynamic>>();
} catch (_) {
return [];
}
}
static bool isEnabled() {
final raw = bind.mainGetLocalOption(key: kShortcutLocalConfigKey);
if (raw.isEmpty) return false;
try {
final parsed = jsonDecode(raw) as Map<String, dynamic>;
return parsed['enabled'] == true;
} catch (_) {
return false;
}
}
}
/// Register the default-bound shortcut actions that aren't already wired by
/// `toolbarControls(...)` (which handles things like Ctrl+Alt+Shift+Del and the
/// screenshot action). Called once per session from the desktop / mobile
/// remote page, after the toolbar registrations have run.
///
/// [tabController] is the desktop window's tab controller; `null` on mobile /
/// web (where tab-switch shortcuts don't apply).
///
/// Each callback below is a no-op when the underlying state required to
/// service the action isn't available (e.g. only one display, only one tab).
void registerSessionShortcutActions(
FFI ffi, {
DesktopTabController? tabController,
}) {
final sessionId = ffi.sessionId;
// Toggle Fullscreen — desktop & web-desktop only. `stateGlobal.setFullscreen`
// handles native window vs. browser fullscreen; on mobile fullscreen is the
// permanent default, so we leave the action unregistered (becomes a logged
// no-op if a mobile user binds it).
if (isDesktop || isWebDesktop) {
ffi.shortcutModel.register(kShortcutActionToggleFullscreen, () {
stateGlobal.setFullscreen(!stateGlobal.fullscreen.value);
});
}
// Switch Display Next / Prev — requires the peer to have at least 2
// displays. No-op when only one display is available or when the user has
// selected the "All displays" pseudo-display.
void switchDisplayBy(int delta) {
final pi = ffi.ffiModel.pi;
final count = pi.displays.length;
if (count <= 1) return;
final current = pi.currentDisplay;
if (current == kAllDisplayValue) return;
final next = ((current + delta) % count + count) % count;
bind.sessionSwitchDisplay(
isDesktop: isDesktop,
sessionId: sessionId,
value: Int32List.fromList([next]),
);
if (pi.isSupportMultiUiSession) {
// On multi-ui-session peers no switch-display message is sent back, so
// update the local state directly (mirrors `model.dart` handling).
ffi.ffiModel.switchToNewDisplay(next, sessionId, ffi.id);
}
}
ffi.shortcutModel.register(kShortcutActionSwitchDisplayNext, () {
switchDisplayBy(1);
});
ffi.shortcutModel.register(kShortcutActionSwitchDisplayPrev, () {
switchDisplayBy(-1);
});
// Switch Tab 1..9 — desktop only. The remote-screen tabs live in the
// window-scoped DesktopTabController, not on the FFI itself, so we need
// the controller from the page that owns this session. No-op on mobile /
// web (no controller passed) and when the requested tab index is out of
// range.
if (tabController != null) {
for (var n = 1; n <= 9; n++) {
final idx = n - 1;
ffi.shortcutModel.register(kShortcutActionSwitchTab(n), () {
if (tabController.state.value.tabs.length > idx) {
tabController.jumpTo(idx);
}
});
}
}
}

View File

@@ -27,30 +27,25 @@ 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 = <String>[];
final _pendingOutputSuppressFlags = <bool>[];
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 _markViewReadyScheduled = false;
bool _suppressTerminalOutput = false;
bool _suppressNextTerminalDataOutput = false;
bool get isPeerWindows => parent.ffiModel.pi.platform == kPeerPlatformWindows;
void Function(int w, int h, int pw, int ph)? onResizeExternal;
Future<void> _handleInput(String data) async {
// 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.
// 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'.
final isMobileOrWebMobile = (isMobile || (isWeb && !isWebDesktop));
if (isMobileOrWebMobile && data == '\n') {
if (isMobileOrWebMobile && isPeerWindows && data == '\n') {
data = '\r';
}
if (_terminalOpened) {
@@ -75,10 +70,7 @@ class TerminalModel with ChangeNotifier {
terminalController = TerminalController();
// Setup terminal callbacks
terminal.onOutput = (data) {
if (_suppressTerminalOutput) return;
_handleInput(data);
};
terminal.onOutput = _handleInput;
terminal.onResize = (w, h, pw, ph) async {
// Validate all dimensions before using them
@@ -92,7 +84,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) {
_scheduleMarkViewReady();
_markViewReady();
}
if (_terminalOpened) {
@@ -118,16 +110,14 @@ class TerminalModel with ChangeNotifier {
void onReady() {
parent.dialogManager.dismissAll();
// 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) {
// Fire and forget - don't block onReady
openTerminal().catchError((e) {
debugPrint('[TerminalModel] Error opening terminal: $e');
});
}
Future<void> openTerminal({bool force = false}) async {
if (_terminalOpened && !force) return;
Future<void> openTerminal() async {
if (_terminalOpened) 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
@@ -285,12 +275,9 @@ class TerminalModel with ChangeNotifier {
if (success) {
_terminalOpened = true;
// 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';
// 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),
@@ -298,7 +285,7 @@ class TerminalModel with ChangeNotifier {
if (!_terminalViewReady &&
terminal.viewWidth > 0 &&
terminal.viewHeight > 0) {
_scheduleMarkViewReady();
_markViewReady();
}
// Process any buffered input
@@ -310,16 +297,12 @@ class TerminalModel with ChangeNotifier {
});
final persistentSessions =
(evt['persistent_sessions'] as List<dynamic>? ?? [])
.whereType<int>()
.where((id) => !parent.terminalModels.containsKey(id))
.toList();
evt['persistent_sessions'] as List<dynamic>? ?? [];
if (kWindowId != null && persistentSessions.isNotEmpty) {
DesktopMultiWindow.invokeMethod(
kWindowId!,
kWindowEventRestoreTerminalSessions,
jsonEncode({
'peer_id': id,
'persistent_sessions': persistentSessions,
}));
}
@@ -349,8 +332,6 @@ class TerminalModel with ChangeNotifier {
final data = evt['data'];
if (data != null) {
final suppressTerminalOutput = _suppressNextTerminalDataOutput;
_suppressNextTerminalDataOutput = false;
try {
String text = '';
if (data is String) {
@@ -370,7 +351,7 @@ class TerminalModel with ChangeNotifier {
return;
}
_writeToTerminal(text, suppressTerminalOutput: suppressTerminalOutput);
_writeToTerminal(text);
} catch (e) {
debugPrint('[TerminalModel] Failed to process terminal data: $e');
}
@@ -380,10 +361,7 @@ 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, {
bool suppressTerminalOutput = false,
}) {
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,
@@ -395,73 +373,34 @@ 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;
}
_writeTerminalChunk(text, suppressTerminalOutput: suppressTerminalOutput);
terminal.write(text);
}
void _flushOutputBuffer() {
if (_pendingOutputChunks.isEmpty) return;
debugPrint(
'[TerminalModel] Flushing $_pendingOutputSize buffered chars (${_pendingOutputChunks.length} chunks)');
for (var i = 0; i < _pendingOutputChunks.length; i++) {
_writeTerminalChunk(
_pendingOutputChunks[i],
suppressTerminalOutput: _pendingOutputSuppressFlags[i],
);
for (final chunk in _pendingOutputChunks) {
terminal.write(chunk);
}
_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;
@@ -487,10 +426,7 @@ 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();
}

View File

@@ -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,30 @@ 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', []);
}
// Mirror of `default_bindings()` in `src/keyboard/shortcuts.rs`. Keep these
// two lists in sync — if you add or change a default binding on the Rust
// side, update the literal below to match.
String mainGetDefaultKeyboardShortcuts({dynamic hint}) {
const prefix = ['primary', 'alt', 'shift'];
final list = <Map<String, dynamic>>[
{'action': 'send_ctrl_alt_del', 'mods': prefix, 'key': 'delete'},
{'action': 'toggle_fullscreen', 'mods': prefix, 'key': 'enter'},
{'action': 'switch_display_next', 'mods': prefix, 'key': 'arrow_right'},
{'action': 'switch_display_prev', 'mods': prefix, 'key': 'arrow_left'},
{'action': 'screenshot', 'mods': prefix, 'key': 'p'},
for (var n = 1; n <= 9; n++)
{'action': 'switch_tab_$n', 'mods': prefix, 'key': 'digit$n'},
];
return jsonEncode(list);
}
String mainGetInputSource({dynamic hint}) {
final inputSource =
js.context.callMethod('getByName', ['option:local', 'input-source']);
@@ -1176,6 +1201,15 @@ class RustdeskImpl {
}
Future<void> 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();
}
@@ -1729,7 +1763,7 @@ class RustdeskImpl {
}
String mainSupportedPrivacyModeImpls({dynamic hint}) {
return '[]';
throw UnimplementedError("mainSupportedPrivacyModeImpls");
}
String mainSupportedInputSource({dynamic hint}) {
@@ -2034,14 +2068,7 @@ class RustdeskImpl {
}
String mainResolveAvatarUrl({required String avatar, dynamic hint}) {
return js.context.callMethod(
'getByName', ['resolve_avatar_url', avatar])?.toString() ??
avatar;
}
Future<String> mainDeployDevice(
{required String token, required String id, dynamic hint}) {
throw UnimplementedError("mainDeployDevice");
return js.context.callMethod('getByName', ['resolve_avatar_url', avatar])?.toString() ?? avatar;
}
void dispose() {}

View File

@@ -1,2 +1,2 @@
#!/usr/bin/env bash
cargo ndk --platform 21 --target armv7-linux-androideabi build --locked --release --features flutter,hwcodec
cargo ndk --platform 21 --target armv7-linux-androideabi build --release --features flutter,hwcodec

View File

@@ -1,2 +1,2 @@
#!/usr/bin/env bash
cargo ndk --platform 21 --target aarch64-linux-android build --locked --release --features flutter,hwcodec
cargo ndk --platform 21 --target aarch64-linux-android build --release --features flutter,hwcodec

View File

@@ -1,2 +1,2 @@
#!/usr/bin/env bash
cargo ndk --platform 21 --target x86_64-linux-android build --locked --release --features flutter
cargo ndk --platform 21 --target x86_64-linux-android build --release --features flutter

View File

@@ -7,4 +7,4 @@
export CFLAGS="-DBROKEN_CLANG_ATOMICS"
export CXXFLAGS="-DBROKEN_CLANG_ATOMICS"
cargo ndk --platform 21 --target i686-linux-android build --locked --release --features flutter
cargo ndk --platform 21 --target i686-linux-android build --release --features flutter

View File

@@ -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.7+65
version: 1.4.6+64
environment:
sdk: '^3.1.0'

View File

@@ -1,9 +1,9 @@
#!/usr/bin/env bash
cargo install flutter_rust_bridge_codegen --version 1.80.1 --features uuid --locked
cargo install flutter_rust_bridge_codegen --version 1.80.1 --features uuid
flutter pub get
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ../src/flutter_ffi.rs --dart-output ./lib/generated_bridge.dart --c-output ./macos/Runner/bridge_generated.h
# call `flutter clean` if cargo build fails
# export LLVM_HOME=/Library/Developer/CommandLineTools/usr/
cargo build --locked --features flutter
cargo build --features flutter
flutter run $@

View File

@@ -39,28 +39,6 @@
#define CLIPRDR_SVC_CHANNEL_NAME "cliprdr"
/* Maximum number of clipboard streams accepted from a remote peer (integer overflow / DoS guard) */
#define WF_CLIPRDR_MAX_STREAMS 16384
/* Validates the remote descriptor array size after cItems has been read safely. */
static BOOL wf_cliprdr_file_group_descriptor_size_valid(SIZE_T size, UINT count)
{
SIZE_T header_size = offsetof(FILEGROUPDESCRIPTORW, fgd);
SIZE_T descriptors_size;
if (count == 0 || count > WF_CLIPRDR_MAX_STREAMS)
return FALSE;
if (size < header_size)
return FALSE;
if ((SIZE_T)count > (((SIZE_T)-1) - header_size) / sizeof(FILEDESCRIPTORW))
return FALSE;
descriptors_size = header_size + (SIZE_T)count * sizeof(FILEDESCRIPTORW);
return size >= descriptors_size;
}
/**
* Clipboard Formats
*/
@@ -246,7 +224,6 @@ struct wf_clipboard
HWND hwnd;
HANDLE hmem;
SIZE_T hmem_data_len;
HANDLE thread;
HANDLE formatDataRespEvent;
BOOL formatDataRespReceived;
@@ -647,55 +624,10 @@ void CliprdrStream_Delete(CliprdrStream *instance)
if (instance)
{
free(instance->iStream.lpVtbl);
instance->iStream.lpVtbl = NULL;
free(instance);
}
}
static void wf_cliprdr_release_streams(IStream **streams, ULONG count)
{
ULONG i;
if (!streams)
return;
for (i = 0; i < count; i++)
{
if (streams[i])
CliprdrStream_Release(streams[i]);
}
free(streams);
}
static void wf_cliprdr_reset_streams(CliprdrDataObject *instance)
{
if (!instance)
return;
wf_cliprdr_release_streams(instance->m_pStream, instance->m_nStreams);
instance->m_pStream = NULL;
instance->m_nStreams = 0;
}
/* Only call after clipboard->hmem has been locked by GlobalLock. */
static HRESULT wf_cliprdr_fail_locked_file_descriptor_data(wfClipboard *clipboard,
STGMEDIUM *medium,
CliprdrDataObject *instance,
IStream **streams,
ULONG stream_count,
HRESULT error)
{
GlobalUnlock(clipboard->hmem);
GlobalFree(clipboard->hmem);
clipboard->hmem = NULL;
clipboard->hmem_data_len = 0;
medium->hGlobal = NULL;
wf_cliprdr_release_streams(streams, stream_count);
wf_cliprdr_reset_streams(instance);
return error;
}
/**
* IDataObject
*/
@@ -814,9 +746,6 @@ static HRESULT STDMETHODCALLTYPE CliprdrDataObject_GetData(IDataObject *This, FO
{
// FILEGROUPDESCRIPTOR *dsc;
FILEGROUPDESCRIPTORW *dsc;
IStream **streams = NULL;
UINT stream_count = 0;
SIZE_T hmem_size;
// DWORD remote_format_id = get_remote_format_id(clipboard, instance->m_pFormatEtc[idx].cfFormat);
// FIXME: origin code may be failed here???
if (cliprdr_send_data_request(instance->m_connID, clipboard, instance->m_pFormatEtc[idx].cfFormat) != 0)
@@ -834,48 +763,40 @@ static HRESULT STDMETHODCALLTYPE CliprdrDataObject_GetData(IDataObject *This, FO
* is the number of FILEDESCRIPTOR's */
// dsc = (FILEGROUPDESCRIPTOR *)GlobalLock(clipboard->hmem);
dsc = (FILEGROUPDESCRIPTORW *)GlobalLock(clipboard->hmem);
if (!dsc)
instance->m_nStreams = dsc->cItems;
GlobalUnlock(clipboard->hmem);
if (instance->m_nStreams > 0)
{
pMedium->hGlobal = NULL;
GlobalFree(clipboard->hmem);
clipboard->hmem = NULL;
clipboard->hmem_data_len = 0;
wf_cliprdr_reset_streams(instance);
return E_UNEXPECTED;
}
hmem_size = clipboard->hmem_data_len;
/* cItems is remote-controlled; verify the fixed header exists before reading it. */
if (hmem_size < offsetof(FILEGROUPDESCRIPTORW, fgd))
return wf_cliprdr_fail_locked_file_descriptor_data(
clipboard, pMedium, instance, NULL, 0, E_UNEXPECTED);
stream_count = dsc->cItems;
if (!wf_cliprdr_file_group_descriptor_size_valid(hmem_size, stream_count))
return wf_cliprdr_fail_locked_file_descriptor_data(
clipboard, pMedium, instance, NULL, 0, E_UNEXPECTED);
streams = (IStream **)calloc(stream_count, sizeof(IStream *));
if (!streams)
return wf_cliprdr_fail_locked_file_descriptor_data(
clipboard, pMedium, instance, NULL, 0, E_OUTOFMEMORY);
for (i = 0; i < stream_count; i++)
{
streams[i] =
(IStream *)CliprdrStream_New(instance->m_connID, i, clipboard, &dsc->fgd[i]);
if (!streams[i])
if (!instance->m_pStream)
{
return wf_cliprdr_fail_locked_file_descriptor_data(
clipboard, pMedium, instance, streams, i, E_OUTOFMEMORY);
instance->m_pStream = (LPSTREAM *)calloc(instance->m_nStreams, sizeof(LPSTREAM));
if (instance->m_pStream)
{
for (i = 0; i < instance->m_nStreams; i++)
{
instance->m_pStream[i] =
(IStream *)CliprdrStream_New(instance->m_connID, i, clipboard, &dsc->fgd[i]);
if (!instance->m_pStream[i])
return E_OUTOFMEMORY;
}
}
}
}
GlobalUnlock(clipboard->hmem);
wf_cliprdr_reset_streams(instance);
instance->m_pStream = streams;
instance->m_nStreams = stream_count;
return S_OK;
if (!instance->m_pStream)
{
if (clipboard->hmem)
{
GlobalFree(clipboard->hmem);
clipboard->hmem = NULL;
}
pMedium->hGlobal = NULL;
return E_OUTOFMEMORY;
}
}
else if (instance->m_pFormatEtc[idx].cfFormat == RegisterClipboardFormat(CFSTR_FILECONTENTS))
{
@@ -2239,16 +2160,16 @@ 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);
if (!clipboard->file_names[clipboard->nFiles])
return FALSE;
// `MAX_PATH` is long enough for the file name.
// So we just return FALSE if the file name is too long, which is not a normal case.
if ((wcslen(full_file_name) + 1) > MAX_PATH)
return FALSE;
clipboard->file_names[clipboard->nFiles] = (LPWSTR)calloc(MAX_PATH, sizeof(WCHAR));
if (!clipboard->file_names[clipboard->nFiles])
return FALSE;
wcsncpy_s(clipboard->file_names[clipboard->nFiles], MAX_PATH, full_file_name, wcslen(full_file_name) + 1);
/* add to descriptor array */
clipboard->fileDescriptor[clipboard->nFiles] =
@@ -2856,7 +2777,6 @@ wf_cliprdr_server_format_data_response(CliprdrClientContext *context,
break;
}
clipboard->hmem = NULL;
clipboard->hmem_data_len = 0;
if (formatDataResponse->msgFlags != CB_RESPONSE_OK)
{
@@ -2890,7 +2810,6 @@ wf_cliprdr_server_format_data_response(CliprdrClientContext *context,
break;
}
clipboard->hmem_data_len = formatDataResponse->dataLen;
clipboard->hmem = hMem;
rc = CHANNEL_RC_OK;
} while (0);

View File

@@ -1,6 +1,6 @@
[package]
name = "rustdesk-portable-packer"
version = "1.4.7"
version = "1.4.6"
edition = "2021"
description = "RustDesk Remote Desktop"

View File

@@ -67,9 +67,9 @@ def write_app_metadata(output_folder: str):
def build_portable(output_folder: str, target: str):
os.chdir(output_folder)
if target:
os.system("cargo build --locked --release --target " + target)
os.system("cargo build --release --target " + target)
else:
os.system("cargo build --locked --release")
os.system("cargo build --release")
# Linux: python3 generate.py -f ../rustdesk-portable-packer/test -o . -e ./test/main.py
# Windows: python3 .\generate.py -f ..\rustdesk\flutter\build\windows\runner\Debug\ -o . -e ..\rustdesk\flutter\build\windows\runner\Debug\rustdesk.exe

View File

@@ -276,21 +276,12 @@ 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, &convert, &sink])?;
src.link(&convert)?;
convert.link(&sink)?;
pipeline.add_many(&[&src, &sink])?;
src.link(&sink)?;
let appsink = sink
.dynamic_cast::<AppSink>()

View File

@@ -1,5 +1,5 @@
pkgname=rustdesk
pkgver=1.4.7
pkgver=1.4.6
pkgrel=0
epoch=
pkgdesc=""

View File

@@ -31,17 +31,17 @@ LExit:
return WcaFinalize(er);
}
// Helper function to safely delete a file using handle-based deletion.
// Directories are refused after opening the handle.
// 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 and attribute-read access plus FILE_FLAG_OPEN_REPARSE_POINT
// 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_READ_ATTRIBUTES,
DELETE,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, // Allow shared access
NULL,
OPEN_EXISTING,
@@ -55,21 +55,6 @@ 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;
@@ -92,74 +77,98 @@ BOOL SafeDeleteItem(LPCWSTR fullPath)
return result;
}
BOOL PathEndsWithSlash(LPCWSTR path)
// Helper function to recursively delete a directory's contents with detailed logging.
void RecursiveDelete(LPCWSTR path)
{
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))
// Ensure the path is not empty or null.
if (path == NULL || path[0] == L'\0')
{
return;
}
DWORD writableAttributes = attributes & ~FILE_ATTRIBUTE_READONLY;
if (writableAttributes == 0)
// Extra safety: never operate directly on a root path.
if (PathIsRootW(path))
{
writableAttributes = FILE_ATTRIBUTE_NORMAL;
}
if (SetFileAttributesW(fullPath, writableAttributes))
{
WcaLog(LOGMSG_STANDARD, "Runtime cleanup cleared read-only attribute for '%ls'.", fullPath);
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: refusing to operate on root path '%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;
// 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;
}
DWORD attributes = GetFileAttributesW(fullPath);
if (attributes == INVALID_FILE_ATTRIBUTES)
WIN32_FIND_DATAW findData;
HANDLE hFind = FindFirstFileW(searchPath, &findData);
if (hFind == INVALID_HANDLE_VALUE)
{
DWORD error = GetLastError();
if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND)
// 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)
{
return TRUE;
continue;
}
WcaLog(LOGMSG_STANDARD, "Runtime cleanup cannot stat '%ls'. Error: %lu", fullPath, error);
return FALSE;
}
// 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;
}
if (attributes & FILE_ATTRIBUTE_DIRECTORY)
// 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, "Runtime cleanup skipped directory '%ls'.", fullPath);
return FALSE;
WcaLog(LOGMSG_STANDARD, "RecursiveDelete: FindNextFileW failed with error %lu", lastError);
}
ClearReadOnlyAttribute(fullPath, attributes);
WcaLog(LOGMSG_STANDARD, "Runtime cleanup deleting '%ls'.", fullPath);
return SafeDeleteItem(fullPath);
FindClose(hFind);
}
// See `Package.wxs` for the sequence of this custom action.
@@ -169,13 +178,13 @@ BOOL DeleteRuntimeGeneratedFile(LPCWSTR installFolder, LPCWSTR fileName)
// 2. RemoveExistingProducts
// ├─ TerminateProcesses
// ├─ TryStopDeleteService
// ├─ RemoveRuntimeGeneratedFiles - <-- Here
// ├─ RemoveInstallFolder - <-- Here
// └─ RemoveFiles
// 3. InstallValidate
// 4. InstallFiles
// 5. InstallExecute
// 6. InstallFinalize
UINT __stdcall RemoveRuntimeGeneratedFiles(
UINT __stdcall RemoveInstallFolder(
__in MSIHANDLE hInstall)
{
HRESULT hr = S_OK;
@@ -185,7 +194,7 @@ UINT __stdcall RemoveRuntimeGeneratedFiles(
LPWSTR pwz = NULL;
LPWSTR pwzData = NULL;
hr = WcaInitialize(hInstall, "RemoveRuntimeGeneratedFiles");
hr = WcaInitialize(hInstall, "RemoveInstallFolder");
ExitOnFailure(hr, "Failed to initialize");
hr = WcaGetProperty(L"CustomActionData", &pwzData);
@@ -193,20 +202,24 @@ UINT __stdcall RemoveRuntimeGeneratedFiles(
pwz = pwzData;
hr = WcaReadStringFromCaData(&pwz, &installFolder);
ExitOnFailure(hr, "failed to read install folder from custom action data: %ls", pwz);
ExitOnFailure(hr, "failed to read database key from custom action data: %ls", pwz);
if (installFolder == NULL || installFolder[0] == L'\0') {
WcaLog(LOGMSG_STANDARD, "Install folder path is empty, skipping runtime cleanup.");
WcaLog(LOGMSG_STANDARD, "Install folder path is empty, skipping recursive delete.");
goto LExit;
}
if (PathIsRootW(installFolder)) {
WcaLog(LOGMSG_STANDARD, "Refusing runtime cleanup in root folder '%ls'.", installFolder);
WcaLog(LOGMSG_STANDARD, "Refusing to recursively delete root folder '%ls'.", installFolder);
goto LExit;
}
WcaLog(LOGMSG_STANDARD, "Removing runtime-generated files from install folder: %ls", installFolder);
DeleteRuntimeGeneratedFile(installFolder, L"RuntimeBroker_rustdesk.exe");
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);

View File

@@ -2,7 +2,7 @@ LIBRARY "CustomActions"
EXPORTS
CustomActionHello
RemoveRuntimeGeneratedFiles
RemoveInstallFolder
TerminateProcesses
AddFirewallRules
SetPropertyIsServiceRunning

View File

@@ -16,15 +16,8 @@
<!-- If a command line value was stored, restore it after the registry search has been performed -->
<SetProperty Action="RestoreSavedInstallFolderValue" Id="INSTALLFOLDER" Value="[SavedInstallFolderCmdLineValue]" After="AppSearch" Sequence="first" Condition="SavedInstallFolderCmdLineValue" />
<!-- Normalize INSTALLFOLDER from the command line or registry before assigning INSTALLFOLDER_INNER. -->
<!-- Case 1: already ends with \$(var.Product)\, keep it unchanged. -->
<SetProperty Action="SetInstallFolderInnerFromProductDir" Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER AND INSTALLFOLDER ~&gt;&gt; &quot;\$(var.Product)\&quot;" />
<!-- Case 2: already ends with \$(var.Product) but has no trailing slash, add the slash. -->
<SetProperty Action="SetInstallFolderInnerFromProductDirNoSlash" Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]\" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER AND INSTALLFOLDER ~&gt;&gt; &quot;\$(var.Product)&quot;" />
<!-- Case 3: ends with a slash but not \$(var.Product)\, append $(var.Product)\. -->
<SetProperty Action="SetInstallFolderInnerAppendProduct" Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]$(var.Product)\" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER AND INSTALLFOLDER ~&gt;&gt; &quot;\&quot; AND NOT (INSTALLFOLDER ~&gt;&gt; &quot;\$(var.Product)\&quot; OR INSTALLFOLDER ~&gt;&gt; &quot;\$(var.Product)&quot;)" />
<!-- Case 4: has no trailing slash and does not end with \$(var.Product), append \$(var.Product)\. -->
<SetProperty Action="SetInstallFolderInnerAppendSlashProduct" Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]\$(var.Product)\" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER AND NOT INSTALLFOLDER ~&gt;&gt; &quot;\&quot; AND NOT (INSTALLFOLDER ~&gt;&gt; &quot;\$(var.Product)\&quot; OR INSTALLFOLDER ~&gt;&gt; &quot;\$(var.Product)&quot;)" />
<!-- If a command line value or registry value was set, update the main properties with the value -->
<SetProperty Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER" />
<!-- INSTALLFOLDER_INNER is defined for compatibility with previous versions of the installer. -->
<!-- Because we need to use INSTALLFOLDER as the command line argument. -->

View File

@@ -12,7 +12,7 @@
</Component>
</DirectoryRef>
<CustomAction Id="RemoveRuntimeGeneratedFiles.SetParam" Return="check" Property="RemoveRuntimeGeneratedFiles" Value="[INSTALLFOLDER_INNER]" />
<CustomAction Id="RemoveInstallFolder.SetParam" Return="check" Property="RemoveInstallFolder" Value="[INSTALLFOLDER_INNER]" />
<CustomAction Id="AddFirewallRules.SetParam" Return="check" Property="AddFirewallRules" Value="1[INSTALLFOLDER_INNER]$(var.Product).exe" />
<CustomAction Id="RemoveFirewallRules.SetParam" Return="check" Property="RemoveFirewallRules" Value="0[INSTALLFOLDER_INNER]$(var.Product).exe" />
<CustomAction Id="CreateStartService.SetParam" Return="check" Property="CreateStartService" Value="$(var.Product);&quot;[INSTALLFOLDER_INNER]$(var.Product).exe&quot; --service" />
@@ -77,21 +77,21 @@
<Custom Action="AddRegSoftwareSASGeneration" Before="InstallFinalize" Condition="NOT (Installed AND REMOVE AND NOT UPGRADINGPRODUCTCODE) AND (NOT CC_CONNECTION_TYPE=&quot;outgoing&quot;)"/>
<Custom Action="RemoveRuntimeGeneratedFiles" Before="RemoveFiles" Condition="Installed AND (REMOVE=&quot;ALL&quot; OR UPGRADINGPRODUCTCODE)"/>
<Custom Action="RemoveRuntimeGeneratedFiles.SetParam" Before="RemoveRuntimeGeneratedFiles" Condition="Installed AND (REMOVE=&quot;ALL&quot; OR UPGRADINGPRODUCTCODE)"/>
<Custom Action="TryStopDeleteService" Before="RemoveRuntimeGeneratedFiles.SetParam" />
<Custom Action="RemoveInstallFolder" Before="RemoveFiles"/>
<Custom Action="RemoveInstallFolder.SetParam" Before="RemoveInstallFolder"/>
<Custom Action="TryStopDeleteService" Before="RemoveInstallFolder.SetParam" />
<Custom Action="TryStopDeleteService.SetParam" Before="TryStopDeleteService" />
<Custom Action="RemoveFirewallRules" Before="RemoveFiles"/>
<Custom Action="RemoveFirewallRules.SetParam" Before="RemoveFirewallRules"/>
<Custom Action="UninstallPrinter" Before="RemoveRuntimeGeneratedFiles" Condition="VersionNT &gt;= 603" />
<Custom Action="UninstallPrinter" Before="RemoveInstallFolder" Condition="VersionNT &gt;= 603" />
<Custom Action="TerminateProcesses" Before="RemoveRuntimeGeneratedFiles"/>
<Custom Action="TerminateProcesses" Before="RemoveInstallFolder"/>
<Custom Action="TerminateProcesses.SetParam" Before="TerminateProcesses"/>
<Custom Action="TerminateBrokers" Before="RemoveRuntimeGeneratedFiles"/>
<Custom Action="TerminateBrokers" Before="RemoveInstallFolder"/>
<Custom Action="TerminateBrokers.SetParam" Before="TerminateBrokers"/>
<Custom Action="RemoveAmyuniIdd" Before="RemoveRuntimeGeneratedFiles"/>
<Custom Action="RemoveAmyuniIdd" Before="RemoveInstallFolder"/>
<Custom Action="RemoveAmyuniIdd.SetParam" Before="RemoveAmyuniIdd"/>
</InstallExecuteSequence>

View File

@@ -5,7 +5,7 @@
<Binary Id="Custom_Actions_Dll" SourceFile="$(var.CustomActions.TargetDir)$(var.CustomActions.TargetName).dll" />
<CustomAction Id="CustomActionHello" DllEntry="CustomActionHello" Impersonate="yes" Execute="immediate" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="RemoveRuntimeGeneratedFiles" DllEntry="RemoveRuntimeGeneratedFiles" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="RemoveInstallFolder" DllEntry="RemoveInstallFolder" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="TerminateProcesses" DllEntry="TerminateProcesses" Impersonate="yes" Execute="immediate" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="TerminateBrokers" DllEntry="TerminateProcesses" Impersonate="yes" Execute="immediate" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="AddFirewallRules" DllEntry="AddFirewallRules" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>

View File

@@ -23,13 +23,12 @@ Patch dialog sequence:
-->
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
<?include ../Includes.wxi?>
<?foreach WIXUIARCH in X86;X64;A64 ?>
<Fragment>
<UI Id="UI_MyInstallDialog_$(WIXUIARCH)">
<Publish Dialog="LicenseAgreementDlg" Control="Print" Event="DoAction" Value="WixUIPrintEula_$(WIXUIARCH)" />
<Publish Dialog="BrowseDlg" Control="OK" Event="DoAction" Value="WixUIValidatePath_$(WIXUIARCH)" Order="3" Condition="NOT WIXUI_DONTVALIDATEPATH" />
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath_$(WIXUIARCH)" Order="5" Condition="NOT WIXUI_DONTVALIDATEPATH" />
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath_$(WIXUIARCH)" Order="2" Condition="NOT WIXUI_DONTVALIDATEPATH" />
</UI>
<UIRef Id="UI_MyInstallDialog" />
@@ -65,16 +64,9 @@ Patch dialog sequence:
<Publish Dialog="LicenseAgreementDlg" Control="Next" Event="NewDialog" Value="MyInstallDirDlg" Condition="LicenseAccepted = &quot;1&quot;" />
<Publish Dialog="MyInstallDirDlg" Control="Back" Event="NewDialog" Value="LicenseAgreementDlg" />
<!-- Normalize INSTALLFOLDER_INNER before SetTargetPath and WixUIValidatePath run. -->
<!-- UI case 1: already ends with \$(var.Product) but has no trailing slash, add the slash. -->
<Publish Dialog="MyInstallDirDlg" Control="Next" Property="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER_INNER]\" Order="1" Condition="INSTALLFOLDER_INNER AND INSTALLFOLDER_INNER ~&gt;&gt; &quot;\$(var.Product)&quot;" />
<!-- UI case 2: ends with a slash but not \$(var.Product)\, append $(var.Product)\. -->
<Publish Dialog="MyInstallDirDlg" Control="Next" Property="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER_INNER]$(var.Product)\" Order="2" Condition="INSTALLFOLDER_INNER AND INSTALLFOLDER_INNER ~&gt;&gt; &quot;\&quot; AND NOT (INSTALLFOLDER_INNER ~&gt;&gt; &quot;\$(var.Product)\&quot; OR INSTALLFOLDER_INNER ~&gt;&gt; &quot;\$(var.Product)&quot;)" />
<!-- UI case 3: has no trailing slash and does not end with \$(var.Product), append \$(var.Product)\. -->
<Publish Dialog="MyInstallDirDlg" Control="Next" Property="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER_INNER]\$(var.Product)\" Order="3" Condition="INSTALLFOLDER_INNER AND NOT INSTALLFOLDER_INNER ~&gt;&gt; &quot;\&quot; AND NOT (INSTALLFOLDER_INNER ~&gt;&gt; &quot;\$(var.Product)\&quot; OR INSTALLFOLDER_INNER ~&gt;&gt; &quot;\$(var.Product)&quot;)" />
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="SetTargetPath" Value="[WIXUI_INSTALLDIR]" Order="4" />
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="SpawnDialog" Value="InvalidDirDlg" Order="6" Condition="NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID&lt;&gt;&quot;1&quot;" />
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="7" Condition="WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID=&quot;1&quot;" />
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="SetTargetPath" Value="[WIXUI_INSTALLDIR]" Order="1" />
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="SpawnDialog" Value="InvalidDirDlg" Order="3" Condition="NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID&lt;&gt;&quot;1&quot;" />
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="4" Condition="WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID=&quot;1&quot;" />
<Publish Dialog="MyInstallDirDlg" Control="ChangeFolder" Property="_BrowseProperty" Value="[WIXUI_INSTALLDIR]" Order="1" />
<Publish Dialog="MyInstallDirDlg" Control="ChangeFolder" Event="SpawnDialog" Value="BrowseDlg" Order="2" />
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MyInstallDirDlg" Order="1" Condition="NOT Installed" />

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash
echo $MACOS_CODESIGN_IDENTITY
cargo install flutter_rust_bridge_codegen --version 1.80.1 --features uuid --locked
cargo install flutter_rust_bridge_codegen --version 1.80.1 --features uuid
cd flutter; flutter pub get; cd -
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h
./build.py --flutter

View File

@@ -1,5 +1,5 @@
Name: rustdesk
Version: 1.4.7
Version: 1.4.6
Release: 0
Summary: RPM package
License: GPL-3.0

View File

@@ -1,5 +1,5 @@
Name: rustdesk
Version: 1.4.7
Version: 1.4.6
Release: 0
Summary: RPM package
License: GPL-3.0

View File

@@ -1,5 +1,5 @@
Name: rustdesk
Version: 1.4.7
Version: 1.4.6
Release: 0
Summary: RPM package
License: GPL-3.0

View File

@@ -1745,9 +1745,6 @@ pub struct LoginConfigHandler {
pub direct: Option<bool>,
pub received: bool,
switch_uuid: Option<String>,
#[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<Mutex<Option<usize>>>,
@@ -1864,11 +1861,6 @@ 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;
@@ -1882,23 +1874,6 @@ 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 {
@@ -3402,36 +3377,6 @@ 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.
///
@@ -3452,22 +3397,12 @@ pub async fn handle_hash(
// Take care of password application order
// switch_uuid
#[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;
}
}
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;
}
}
// last password

View File

@@ -1797,9 +1797,6 @@ impl<T: InvokeUiSession> Remote<T> {
Ok(Permission::BlockInput) => {
self.handler.set_permission("block_input", p.enabled);
}
Ok(Permission::PrivacyMode) => {
self.handler.set_permission("privacy_mode", p.enabled);
}
_ => {}
}
}
@@ -1923,23 +1920,9 @@ impl<T: InvokeUiSession> Remote<T> {
);
}
}
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
Some(misc::Union::SwitchBack(_)) => {
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(feature = "flutter")]
self.handler.switch_back(&self.handler.get_id());
}
#[cfg(all(feature = "flutter", feature = "plugin_framework"))]
#[cfg(not(any(target_os = "android", target_os = "ios")))]

View File

@@ -1,7 +1,5 @@
#[cfg(not(target_os = "android"))]
use arboard::{ClipboardData, ClipboardFormat};
#[cfg(target_os = "linux")]
use arboard::{LinuxClipboardKind, SetExtLinux};
use hbb_common::{bail, log, message_proto::*, ResultType};
use std::{
sync::{Arc, Mutex},
@@ -56,27 +54,6 @@ pub fn check_clipboard(
side: ClipboardSide,
force: bool,
) -> Option<Message> {
let (msg, clipboards) = read_clipboard_message(ctx, side, force)?;
*LAST_MULTI_CLIPBOARDS.lock().unwrap() = clipboards;
Some(msg)
}
#[cfg(target_os = "linux")]
pub fn peek_clipboard(
ctx: &mut Option<ClipboardContext>,
side: ClipboardSide,
force: bool,
) -> Option<Message> {
let (msg, _) = read_clipboard_message(ctx, side, force)?;
Some(msg)
}
#[cfg(not(target_os = "android"))]
fn read_clipboard_message(
ctx: &mut Option<ClipboardContext>,
side: ClipboardSide,
force: bool,
) -> Option<(Message, MultiClipboards)> {
if ctx.is_none() {
*ctx = ClipboardContext::new().ok();
}
@@ -87,7 +64,8 @@ fn read_clipboard_message(
let mut msg = Message::new();
let clipboards = proto::create_multi_clipboards(content);
msg.set_multi_clipboards(clipboards.clone());
return Some((msg, clipboards));
*LAST_MULTI_CLIPBOARDS.lock().unwrap() = clipboards;
return Some(msg);
}
}
Err(e) => {
@@ -241,7 +219,10 @@ fn do_update_clipboard_(mut to_update_data: Vec<ClipboardData>, side: ClipboardS
}
}
if let Some(ctx) = ctx.as_mut() {
to_update_data = append_owner_marker(to_update_data, side);
to_update_data.push(ClipboardData::Special((
RUSTDESK_CLIPBOARD_OWNER_FORMAT.to_owned(),
side.get_owner_data(),
)));
if let Err(e) = ctx.set(&to_update_data) {
log::debug!("Failed to set clipboard: {}", e);
} else {
@@ -250,29 +231,6 @@ fn do_update_clipboard_(mut to_update_data: Vec<ClipboardData>, side: ClipboardS
}
}
#[cfg(not(target_os = "android"))]
fn append_owner_marker(mut data: Vec<ClipboardData>, side: ClipboardSide) -> Vec<ClipboardData> {
data.push(ClipboardData::Special((
RUSTDESK_CLIPBOARD_OWNER_FORMAT.to_owned(),
side.get_owner_data(),
)));
data
}
#[cfg(target_os = "linux")]
pub fn set_text_clipboard_with_owner_sync(text: &str, side: ClipboardSide) -> ResultType<()> {
let mut ctx = CLIPBOARD_CTX.lock().unwrap();
if ctx.is_none() {
*ctx = Some(ClipboardContext::new()?);
}
let clipboard_ctx = match ctx.as_mut() {
Some(ctx) => ctx,
None => bail!("Failed to create clipboard context"),
};
let data = append_owner_marker(vec![ClipboardData::Text(text.to_owned())], side);
clipboard_ctx.set_with_owner_marker_for_linux(&data)
}
#[cfg(not(target_os = "android"))]
pub fn update_clipboard(multi_clipboards: Vec<Clipboard>, side: ClipboardSide) {
std::thread::spawn(move || {
@@ -424,24 +382,6 @@ impl ClipboardContext {
Ok(())
}
#[cfg(target_os = "linux")]
fn set_with_owner_marker_for_linux(&mut self, data: &[ClipboardData]) -> ResultType<()> {
let _lock = ARBOARD_MTX.lock().unwrap();
self.inner
.set()
.clipboard(LinuxClipboardKind::Clipboard)
.formats(data)?;
if let Err(e) = self
.inner
.set()
.clipboard(LinuxClipboardKind::Primary)
.formats(data)
{
log::warn!("Failed to set PRIMARY clipboard with owner marker: {}", e);
}
Ok(())
}
#[cfg(all(feature = "unix-file-copy-paste", target_os = "macos"))]
fn get_file_urls_set_by_rustdesk(
data: Vec<ClipboardData>,

View File

@@ -146,13 +146,7 @@ pub fn core_main() -> Option<Vec<String>> {
crate::portable_service::client::set_quick_support(_is_quick_support);
}
let mut log_name = "".to_owned();
// 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("--") {
if args.len() > 0 && args[0].starts_with("--") {
let name = args[0].replace("--", "");
if !name.is_empty() {
log_name = name;
@@ -199,20 +193,6 @@ pub fn core_main() -> Option<Vec<String>> {
}
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/<app>-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;
@@ -432,7 +412,7 @@ pub fn core_main() -> Option<Vec<String>> {
}
return None;
} else if args[0] == "--password" {
if is_cli_setting_change_disabled() {
if config::is_disable_settings() {
println!("Settings are disabled!");
return None;
}
@@ -474,7 +454,7 @@ pub fn core_main() -> Option<Vec<String>> {
println!("{}", crate::ipc::get_id());
return None;
} else if args[0] == "--set-id" {
if is_cli_setting_change_disabled() {
if config::is_disable_settings() {
println!("Settings are disabled!");
return None;
}
@@ -521,7 +501,7 @@ pub fn core_main() -> Option<Vec<String>> {
}
return None;
} else if args[0] == "--option" {
if is_cli_setting_change_disabled() {
if config::is_disable_settings() {
println!("Settings are disabled!");
return None;
}
@@ -641,56 +621,6 @@ pub fn core_main() -> Option<Vec<String>> {
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 config::is_outgoing_only() {
println!("Cannot deploy Outgoing-only clients.");
} 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");
match crate::ui_interface::deploy_device(token, new_id) {
crate::ui_interface::DeployResult::Ok => {
println!("Device deployed.");
}
crate::ui_interface::DeployResult::NotEnabled => {
println!("Server does not require deployment.");
std::process::exit(3);
}
crate::ui_interface::DeployResult::InvalidInput => {
println!("Invalid input.");
std::process::exit(5);
}
crate::ui_interface::DeployResult::IdTaken(id) => {
println!(
"Id `{}` is already used by another machine on the server.",
id
);
std::process::exit(6);
}
crate::ui_interface::DeployResult::Error(err) => {
println!("{}", err);
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();
@@ -910,65 +840,6 @@ 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")
| Some("--deploy")
)
}
#[inline]
fn is_cli_setting_change_disabled() -> bool {
let option = config::keys::OPTION_ALLOW_COMMAND_LINE_SETTINGS_WHEN_SETTINGS_DISABLED;
let allow_command_line_settings =
config::option2bool(option, &crate::get_builtin_option(option));
config::is_disable_settings() && !allow_command_line_settings
}
#[cfg(test)]
mod tests {
use super::*;
fn args(values: &[&str]) -> Vec<String> {
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",
"--deploy",
] {
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)]

View File

@@ -1135,10 +1135,6 @@ 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)));

View File

@@ -575,6 +575,7 @@ pub fn session_handle_flutter_key_event(
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
let keyboard_mode = session.get_keyboard_mode();
session.handle_flutter_key_event(
session_id,
&keyboard_mode,
&character,
usb_hid,
@@ -595,6 +596,7 @@ pub fn session_handle_flutter_raw_key_event(
if let Some(session) = sessions::get_session_by_session_id(&session_id) {
let keyboard_mode = session.get_keyboard_mode();
session.handle_flutter_raw_key_event(
session_id,
&keyboard_mode,
&name,
platform_code,
@@ -972,27 +974,6 @@ pub fn main_show_option(_key: String) -> SyncReturn<bool> {
}
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(
@@ -1040,29 +1021,7 @@ pub fn main_get_options_sync() -> SyncReturn<String> {
}
pub fn main_set_options(json: String) {
let mut map: HashMap<String, String> = 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
);
}
}
}
}
let map: HashMap<String, String> = serde_json::from_str(&json).unwrap_or(HashMap::new());
if !map.is_empty() {
set_options(map)
}
@@ -1153,22 +1112,6 @@ pub fn main_get_api_server() -> String {
get_api_server()
}
pub fn main_deploy_device(token: String, id: String) -> String {
#[cfg(target_os = "android")]
{
let new_id = match id.trim() {
"" => None,
id => Some(id.to_owned()),
};
ui_interface::deploy_device(token, new_id).message()
}
#[cfg(not(target_os = "android"))]
{
let _ = (token, id);
"Deployment is not supported on this platform.".to_owned()
}
}
pub fn main_resolve_avatar_url(avatar: String) -> SyncReturn<String> {
SyncReturn(resolve_avatar_url(avatar))
}
@@ -1787,6 +1730,7 @@ pub fn cm_get_clients_length() -> usize {
pub fn main_init(app_dir: String, custom_client_config: String) {
initialize(&app_dir, &custom_client_config);
crate::keyboard::shortcuts::reload_from_config();
}
pub fn main_device_id(id: String) {
@@ -2132,7 +2076,6 @@ pub fn main_start_service() {
#[cfg(target_os = "android")]
{
config::Config::set_option("stop-service".into(), "".into());
crate::rendezvous_mediator::reset_needs_deploy_notification();
crate::rendezvous_mediator::RendezvousMediator::restart();
}
}
@@ -2230,7 +2173,7 @@ pub fn cm_elevate_portable(conn_id: i32) {
}
pub fn cm_switch_back(conn_id: i32) {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
#[cfg(not(any(target_os = "ios")))]
crate::ui_cm_interface::switch_back(conn_id);
}
@@ -2307,6 +2250,17 @@ pub fn main_init_input_source() -> SyncReturn<()> {
SyncReturn(())
}
pub fn main_reload_keyboard_shortcuts() -> SyncReturn<()> {
crate::keyboard::shortcuts::reload_from_config();
SyncReturn(())
}
pub fn main_get_default_keyboard_shortcuts() -> SyncReturn<String> {
let bindings = crate::keyboard::shortcuts::default_bindings();
let json = serde_json::to_string(&bindings).unwrap_or_default();
SyncReturn(json)
}
pub fn main_is_installed_lower_version() -> SyncReturn<bool> {
SyncReturn(is_installed_lower_version())
}
@@ -2488,13 +2442,23 @@ pub fn is_disable_installation() -> SyncReturn<bool> {
}
pub fn is_preset_password() -> bool {
let hard = config::HARD_SETTINGS
.read()
.unwrap()
.get("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::is_using_preset_password();
return config::Config::matches_permanent_password_plain(&hard);
}
// Don't call this function for desktop version.
@@ -3072,7 +3036,6 @@ pub mod server_side {
pub unsafe extern "system" fn Java_ffi_FFI_startService(_env: JNIEnv, _class: JClass) {
log::debug!("startService from jvm");
config::Config::set_option("stop-service".into(), "".into());
crate::rendezvous_mediator::reset_needs_deploy_notification();
crate::rendezvous_mediator::RendezvousMediator::restart();
}

View File

@@ -1,28 +1,33 @@
#[path = "ipc/auth.rs"]
mod ipc_auth;
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[path = "ipc/fs.rs"]
mod ipc_fs;
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::*};
#[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::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,
@@ -33,92 +38,13 @@ use hbb_common::{
tokio_util::codec::Framed,
ResultType,
};
#[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(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::{
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::cell::Cell;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use std::os::unix::fs::PermissionsExt;
use std::{
collections::HashMap,
sync::atomic::{AtomicBool, Ordering},
};
use crate::{common::is_server, privacy_mode, rendezvous_mediator::RendezvousMediator};
// 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<bool> = 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<ConnectionTmpl<ConnClient>> {
connect(ms_timeout, crate::POSTFIX_SERVICE).await
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "t", content = "c")]
pub enum FS {
@@ -281,8 +207,6 @@ pub enum DataControl {
pub enum DataPortableService {
Ping,
Pong,
AuthToken(String),
AuthResult(bool),
ConnCount(Option<usize>),
Mouse((Vec<u8>, i32, String, u32, bool, bool)),
Pointer((Vec<u8>, i32)),
@@ -313,7 +237,6 @@ pub enum Data {
restart: bool,
recording: bool,
block_input: bool,
privacy_mode: bool,
from_switch: bool,
},
ChatMessage {
@@ -349,7 +272,6 @@ pub enum Data {
ClipboardNonFile(Option<(String, Vec<ClipboardNonFile>)>),
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")))]
@@ -362,14 +284,7 @@ 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<bool>),
#[cfg(feature = "flutter")]
#[cfg(not(any(target_os = "android", target_os = "ios")))]
SwitchSidesBack,
UrlLink(String),
VoiceCallIncoming,
@@ -488,22 +403,6 @@ 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 {
@@ -512,48 +411,9 @@ 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;
}
}
}
_ => {}
}
}
});
@@ -568,77 +428,20 @@ pub async fn start(postfix: &str) -> ResultType<()> {
pub async fn new_listener(postfix: &str) -> ResultType<Incoming> {
let path = Config::ipc_path(postfix);
#[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)?;
}
#[cfg(not(any(windows, target_os = "android", target_os = "ios")))]
check_pid(postfix).await;
let mut endpoint = Endpoint::new(path.clone());
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 {
match SecurityAttributes::allow_everyone_create() {
Ok(attr) => endpoint.set_security_attributes(attr),
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());
}
}
Err(err) => log::error!("Failed to set ipc{} security: {}", postfix, err),
};
match endpoint.incoming() {
Ok(incoming) => {
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"))]
log::info!("Started ipc{} server at path: {}", postfix, &path);
#[cfg(not(windows))]
{
// 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());
}
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o0777)).ok();
write_pid(postfix);
}
Ok(incoming)
@@ -839,7 +642,15 @@ async fn handle(data: Data, stream: &mut Connection) {
"N".to_owned()
});
} else if name == "permanent-password-is-preset" {
value = Some(if Config::is_using_preset_password() {
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()
@@ -890,7 +701,7 @@ async fn handle(data: Data, stream: &mut Connection) {
log::warn!("Changing permanent password is disabled");
updated = false;
} else {
updated = Config::set_permanent_password(&value);
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
@@ -959,12 +770,6 @@ 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) => {
let uuid = uuid::Uuid::new_v4();
crate::server::insert_switch_sides_uuid(id, uuid.clone());
@@ -974,19 +779,6 @@ 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::<uuid::Uuid>()
.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,
@@ -1138,210 +930,13 @@ async fn handle(data: Data, stream: &mut Connection) {
);
}
_ => {}
};
}
#[cfg(target_os = "windows")]
pub(crate) fn generate_one_time_ipc_token() -> ResultType<String> {
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)
}
#[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;
}
expected
.as_bytes()
.iter()
.zip(candidate.as_bytes().iter())
.fold(0u8, |diff, (left, right)| diff | (*left ^ *right))
== 0
}
#[cfg(target_os = "windows")]
pub(crate) async fn portable_service_ipc_handshake_as_client<T>(
stream: &mut ConnectionTmpl<T>,
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"),
}
}
#[cfg(target_os = "windows")]
pub(crate) async fn portable_service_ipc_handshake_as_server<T, F>(
stream: &mut ConnectionTmpl<T>,
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<ConnectionTmpl<ConnClient>> {
let client = timeout(ms_timeout, Endpoint::connect(path)).await??;
Ok(ConnectionTmpl::new(client))
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[inline]
fn select_server_uid_for_user_main_ipc(
server_uids: &[u32],
active_uid: Option<u32>,
prefer_root: bool,
) -> ResultType<u32> {
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<Vec<u32>> {
let current_exe = std::env::current_exe()?;
let current_exe_path = std::fs::canonicalize(&current_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<u32> {
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<ConnectionTmpl<ConnClient>> {
#[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,
uid: u32,
postfix: &str,
) -> ResultType<ConnectionTmpl<ConnClient>> {
let path = Config::ipc_path_for_uid(uid, postfix);
connect_with_path(ms_timeout, &path).await
let path = Config::ipc_path(postfix);
let client = timeout(ms_timeout, Endpoint::connect(&path)).await??;
Ok(ConnectionTmpl::new(client))
}
#[cfg(target_os = "linux")]
@@ -1421,6 +1016,54 @@ 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::<usize>().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<T> {
inner: Framed<T, BytesCodec>,
}
@@ -1542,6 +1185,11 @@ fn apply_permanent_password_storage_and_salt_payload(payload: Option<&str>) -> R
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(())
}
@@ -1862,13 +1510,6 @@ 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")
@@ -1886,10 +1527,9 @@ pub fn close_all_instances() -> ResultType<bool> {
}
}
#[cfg(windows)]
#[tokio::main(flavor = "current_thread")]
pub async fn connect_to_user_session(usid: Option<u32>) -> ResultType<()> {
let mut stream = crate::ipc::connect_service(1000).await?;
let mut stream = crate::ipc::connect(1000, crate::POSTFIX_SERVICE).await?;
timeout(1000, stream.send(&crate::ipc::Data::UserSid(usid))).await??;
Ok(())
}
@@ -2015,76 +1655,13 @@ 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<usize> {
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<anyhow::Error> = 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)
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);
}
Ok(0)
}
async fn handle_wayland_screencast_restore_token(
@@ -2115,81 +1692,9 @@ 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::<Data>());
assert!(std::mem::size_of::<Data>() <= 120);
}
#[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 };
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)
);
}
#[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());
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,951 +0,0 @@
#[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<u32> {
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<i32> {
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<bool> {
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<usize> {
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::<usize>().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
));
}
}

View File

@@ -10,6 +10,7 @@ use crate::{client::get_key_state, common::GrabState};
#[cfg(not(any(target_os = "android", target_os = "ios")))]
use hbb_common::log;
use hbb_common::message_proto::*;
use hbb_common::SessionID;
#[cfg(any(target_os = "windows", target_os = "macos"))]
use rdev::KeyCode;
use rdev::{Event, EventType, Key};
@@ -79,6 +80,8 @@ lazy_static::lazy_static! {
};
}
pub mod shortcuts;
pub mod client {
use super::*;
@@ -319,6 +322,33 @@ pub mod client {
}
pub fn process_event(keyboard_mode: &str, event: &Event, lock_modes: Option<i32>) {
// Shortcut intercept — must come before any wire encoding.
// Only fires on KeyPress (event_to_key_name in shortcuts.rs returns None
// for KeyRelease and other non-press events), so flushed releases from
// release_remote_keys pass straight through to the encode/forward path.
if let Some(action_id) = crate::keyboard::shortcuts::match_event(event) {
#[cfg(feature = "flutter")]
{
// The rdev grab loop is genuinely process-wide: it does not know which
// Flutter SessionID the keystroke was meant for, so we route to the
// globally-current session via flutter::get_cur_session_id() (maintained
// by session_enter_or_leave). This is the only behavior available on the
// rdev path; the Flutter path threads the explicit per-call SessionID
// through process_event_with_session instead.
let session_id = crate::flutter::get_cur_session_id();
crate::flutter::push_session_event(
&session_id,
"shortcut_triggered",
vec![("action", &action_id)],
);
}
#[cfg(not(feature = "flutter"))]
{
let _ = action_id;
}
return;
}
let keyboard_mode = get_keyboard_mode_enum(keyboard_mode);
if is_long_press(&event) {
return;
@@ -334,7 +364,33 @@ pub mod client {
event: &Event,
lock_modes: Option<i32>,
session: &Session<T>,
session_id: SessionID,
) {
// Shortcut intercept — must come before any wire encoding.
// Only fires on KeyPress (event_to_key_name in shortcuts.rs returns None
// for KeyRelease and other non-press events), so flushed releases from
// release_remote_keys pass straight through to the encode/forward path.
if let Some(action_id) = crate::keyboard::shortcuts::match_event(event) {
#[cfg(feature = "flutter")]
{
// The Flutter path threads the explicit SessionID from the FFI entry
// (session_handle_flutter_*key_event) through this call, so the dispatch
// targets the exact tab the keystroke originated from — no dependency on
// the global focus tracker and no multi-window race.
crate::flutter::push_session_event(
&session_id,
"shortcut_triggered",
vec![("action", &action_id)],
);
}
#[cfg(not(feature = "flutter"))]
{
let _ = action_id;
let _ = session_id;
}
return;
}
let keyboard_mode = get_keyboard_mode_enum(keyboard_mode);
if is_long_press(&event) {
return;

370
src/keyboard/shortcuts.rs Normal file
View File

@@ -0,0 +1,370 @@
//! Keyboard shortcuts for triggering session actions locally.
use std::sync::{Arc, RwLock};
use serde::{Deserialize, Serialize};
const LOCAL_CONFIG_KEY: &str = "keyboard-shortcuts";
lazy_static::lazy_static! {
static ref CACHE: RwLock<Arc<Bindings>> = RwLock::new(Arc::new(Bindings::default()));
}
/// Registry of all valid action ids that may appear in `Binding.action`.
/// Source-of-truth lives on the Flutter side (`flutter/lib/consts.dart`,
/// `kShortcutAction*`); these mirror that vocabulary so Rust code can reach
/// for them without re-stringifying.
#[allow(dead_code)]
pub mod action_id {
pub const SEND_CTRL_ALT_DEL: &str = "send_ctrl_alt_del";
pub const TOGGLE_FULLSCREEN: &str = "toggle_fullscreen";
pub const SWITCH_DISPLAY_NEXT: &str = "switch_display_next";
pub const SWITCH_DISPLAY_PREV: &str = "switch_display_prev";
pub const SCREENSHOT: &str = "screenshot";
pub const INSERT_LOCK: &str = "insert_lock";
pub const REFRESH: &str = "refresh";
pub const TOGGLE_AUDIO: &str = "toggle_audio";
pub const TOGGLE_BLOCK_INPUT: &str = "toggle_block_input";
pub const TOGGLE_RECORDING: &str = "toggle_recording";
pub const TOGGLE_PRIVACY_MODE: &str = "toggle_privacy_mode";
pub const VIEW_MODE_1_TO_1: &str = "view_mode_1_to_1";
pub const VIEW_MODE_SHRINK: &str = "view_mode_shrink";
pub const VIEW_MODE_STRETCH: &str = "view_mode_stretch";
pub const SWITCH_SIDES: &str = "switch_sides";
// switch_tab_1 .. switch_tab_9 are generated below.
}
pub fn switch_tab_action_id(n: u8) -> Option<&'static str> {
match n {
1 => Some("switch_tab_1"),
2 => Some("switch_tab_2"),
3 => Some("switch_tab_3"),
4 => Some("switch_tab_4"),
5 => Some("switch_tab_5"),
6 => Some("switch_tab_6"),
7 => Some("switch_tab_7"),
8 => Some("switch_tab_8"),
9 => Some("switch_tab_9"),
_ => None,
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Modifier {
Primary,
Alt,
Shift,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Binding {
pub action: String,
pub mods: Vec<Modifier>,
pub key: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct Bindings {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub bindings: Vec<Binding>,
}
pub fn default_bindings() -> Vec<Binding> {
let prefix = || vec![Modifier::Primary, Modifier::Alt, Modifier::Shift];
let mut v = vec![
Binding { action: action_id::SEND_CTRL_ALT_DEL.into(), mods: prefix(), key: "delete".into() },
Binding { action: action_id::TOGGLE_FULLSCREEN.into(), mods: prefix(), key: "enter".into() },
Binding { action: action_id::SWITCH_DISPLAY_NEXT.into(), mods: prefix(), key: "arrow_right".into() },
Binding { action: action_id::SWITCH_DISPLAY_PREV.into(), mods: prefix(), key: "arrow_left".into() },
Binding { action: action_id::SCREENSHOT.into(), mods: prefix(), key: "p".into() },
];
for n in 1..=9u8 {
if let Some(action) = switch_tab_action_id(n) {
v.push(Binding {
action: action.into(),
mods: prefix(),
key: format!("digit{n}"),
});
}
}
v
}
/// Match a normalized (key, modifiers) pair against the given bindings.
/// Returns the matched action ID, or None.
pub fn match_normalized<'a>(key: &str, mods: &[Modifier], b: &'a Bindings) -> Option<&'a str> {
if !b.enabled {
return None;
}
for binding in &b.bindings {
if binding.key == key && mods_equal(&binding.mods, mods) {
return Some(binding.action.as_str());
}
}
None
}
pub fn normalize_modifiers(alt: bool, ctrl: bool, shift: bool, command: bool) -> Vec<Modifier> {
let mut v = Vec::new();
let primary = if cfg!(target_os = "macos") { command } else { ctrl };
if primary { v.push(Modifier::Primary); }
if alt { v.push(Modifier::Alt); }
if shift { v.push(Modifier::Shift); }
v
}
/// Map an rdev::Event to a string key name, matching the storage schema.
/// Returns None for events we don't intercept (modifier-only presses, releases, etc.).
pub fn event_to_key_name(event: &rdev::Event) -> Option<String> {
use rdev::{EventType, Key};
let key = match event.event_type {
EventType::KeyPress(k) => k,
_ => return None,
};
Some(match key {
Key::Delete => "delete".into(),
Key::Return => "enter".into(),
Key::LeftArrow => "arrow_left".into(),
Key::RightArrow => "arrow_right".into(),
Key::UpArrow => "arrow_up".into(),
Key::DownArrow => "arrow_down".into(),
Key::KeyA => "a".into(),
Key::KeyB => "b".into(),
Key::KeyC => "c".into(),
Key::KeyD => "d".into(),
Key::KeyE => "e".into(),
Key::KeyF => "f".into(),
Key::KeyG => "g".into(),
Key::KeyH => "h".into(),
Key::KeyI => "i".into(),
Key::KeyJ => "j".into(),
Key::KeyK => "k".into(),
Key::KeyL => "l".into(),
Key::KeyM => "m".into(),
Key::KeyN => "n".into(),
Key::KeyO => "o".into(),
Key::KeyP => "p".into(),
Key::KeyQ => "q".into(),
Key::KeyR => "r".into(),
Key::KeyS => "s".into(),
Key::KeyT => "t".into(),
Key::KeyU => "u".into(),
Key::KeyV => "v".into(),
Key::KeyW => "w".into(),
Key::KeyX => "x".into(),
Key::KeyY => "y".into(),
Key::KeyZ => "z".into(),
Key::Num1 => "digit1".into(),
Key::Num2 => "digit2".into(),
Key::Num3 => "digit3".into(),
Key::Num4 => "digit4".into(),
Key::Num5 => "digit5".into(),
Key::Num6 => "digit6".into(),
Key::Num7 => "digit7".into(),
Key::Num8 => "digit8".into(),
Key::Num9 => "digit9".into(),
_ => return None,
})
}
/// Read keyboard-shortcut bindings from `LocalConfig` and refresh the cache.
///
/// Empty or invalid JSON falls back to `Bindings::default()` (disabled, no
/// bindings). Call this once at startup and again whenever the config is
/// written.
pub fn reload_from_config() {
let raw = hbb_common::config::LocalConfig::get_option(LOCAL_CONFIG_KEY);
let parsed = if raw.is_empty() {
Bindings::default()
} else {
serde_json::from_str(&raw).unwrap_or_default()
};
if let Ok(mut w) = CACHE.write() {
*w = Arc::new(parsed);
}
}
/// Snapshot of the currently cached bindings. Cheap (one atomic increment) —
/// safe to call on every keystroke.
pub fn current() -> Arc<Bindings> {
CACHE
.read()
.map(|b| Arc::clone(&b))
.unwrap_or_else(|_| Arc::new(Bindings::default()))
}
/// Match an `rdev::Event` against the cached bindings. Returns the matched
/// action id, or `None` if no binding fires. The Flutter side ignores unknown
/// action ids (logged as "no handler"), so no whitelist check is needed here.
pub fn match_event(event: &rdev::Event) -> Option<String> {
let bindings = current();
if !bindings.enabled {
return None;
}
let key_name = event_to_key_name(event)?;
let (alt, ctrl, shift, command) =
crate::keyboard::client::get_modifiers_state(false, false, false, false);
let mods = normalize_modifiers(alt, ctrl, shift, command);
match_normalized(&key_name, &mods, &bindings).map(str::to_owned)
}
fn mods_bits(m: &[Modifier]) -> u8 {
let mut bits = 0u8;
for x in m {
bits |= match x {
Modifier::Primary => 1,
Modifier::Alt => 2,
Modifier::Shift => 4,
};
}
bits
}
fn mods_equal(a: &[Modifier], b: &[Modifier]) -> bool {
mods_bits(a) == mods_bits(b)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bindings_round_trip_json() {
let json = r#"{
"enabled": true,
"bindings": [
{"action": "send_ctrl_alt_del", "mods": ["primary","alt","shift"], "key": "delete"},
{"action": "toggle_fullscreen", "mods": ["primary","alt","shift"], "key": "enter"}
]
}"#;
let parsed: Bindings = serde_json::from_str(json).expect("parse");
assert!(parsed.enabled);
assert_eq!(parsed.bindings.len(), 2);
assert_eq!(parsed.bindings[0].action, "send_ctrl_alt_del");
assert_eq!(parsed.bindings[0].key, "delete");
let serialized = serde_json::to_string(&parsed).expect("serialize");
let reparsed: Bindings = serde_json::from_str(&serialized).expect("reparse");
assert_eq!(parsed, reparsed);
}
#[test]
fn defaults_match_design_doc() {
let defaults = default_bindings();
let actions: Vec<&str> = defaults.iter().map(|b| b.action.as_str()).collect();
assert!(actions.contains(&action_id::SEND_CTRL_ALT_DEL));
assert!(actions.contains(&action_id::TOGGLE_FULLSCREEN));
assert!(actions.contains(&action_id::SWITCH_DISPLAY_NEXT));
assert!(actions.contains(&action_id::SWITCH_DISPLAY_PREV));
assert!(actions.contains(&action_id::SCREENSHOT));
assert!(actions.contains(&"switch_tab_1"));
assert!(actions.contains(&"switch_tab_9"));
// every default binding includes the three-modifier prefix
for b in &defaults {
assert!(b.mods.contains(&Modifier::Primary));
assert!(b.mods.contains(&Modifier::Alt));
assert!(b.mods.contains(&Modifier::Shift));
}
}
fn match_for_test<'a>(key: &str, mods: &[Modifier], b: &'a Bindings) -> Option<&'a str> {
match_normalized(key, mods, b)
}
#[test]
fn match_returns_none_when_disabled() {
let bindings = Bindings { enabled: false, bindings: default_bindings() };
let result = match_for_test("p", &[Modifier::Primary, Modifier::Alt, Modifier::Shift], &bindings);
assert_eq!(result, None);
}
#[test]
fn match_screenshot_when_enabled() {
let bindings = Bindings { enabled: true, bindings: default_bindings() };
let result = match_for_test("p", &[Modifier::Primary, Modifier::Alt, Modifier::Shift], &bindings);
assert_eq!(result, Some(action_id::SCREENSHOT));
}
#[test]
fn match_returns_none_when_modifiers_partial() {
let bindings = Bindings { enabled: true, bindings: default_bindings() };
// missing Shift
let result = match_for_test("p", &[Modifier::Primary, Modifier::Alt], &bindings);
assert_eq!(result, None);
}
#[test]
fn match_does_not_fire_on_extra_unbound_keys() {
let bindings = Bindings { enabled: true, bindings: default_bindings() };
let result = match_for_test("z", &[Modifier::Primary, Modifier::Alt, Modifier::Shift], &bindings);
assert_eq!(result, None);
}
#[test]
fn match_handles_duplicate_modifiers_in_input() {
// A user-edited config could contain duplicate modifiers; the matcher must
// treat the modifier list as a set, not a multiset.
let bindings = Bindings {
enabled: true,
bindings: vec![Binding {
action: "x".into(),
mods: vec![Modifier::Primary, Modifier::Alt],
key: "a".into(),
}],
};
// Caller passes Primary twice — must not match a binding with Primary+Alt.
assert_eq!(
match_normalized("a", &[Modifier::Primary, Modifier::Primary], &bindings),
None,
);
// Caller passes Primary+Alt with one duplicate — should still match.
assert_eq!(
match_normalized("a", &[Modifier::Primary, Modifier::Alt, Modifier::Alt], &bindings),
Some("x"),
);
}
#[test]
fn modifier_normalization_primary_resolves_per_os() {
// On Win/Linux: pressing Ctrl satisfies Primary
let mods = normalize_modifiers(/*alt=*/true, /*ctrl=*/true, /*shift=*/true, /*command=*/false);
if cfg!(target_os = "macos") {
// On macOS Ctrl is NOT primary
assert!(!mods.contains(&Modifier::Primary));
} else {
assert!(mods.contains(&Modifier::Primary));
}
assert!(mods.contains(&Modifier::Alt));
assert!(mods.contains(&Modifier::Shift));
}
#[test]
fn modifier_normalization_command_is_primary_on_mac() {
let mods = normalize_modifiers(true, false, true, /*command=*/true);
if cfg!(target_os = "macos") {
assert!(mods.contains(&Modifier::Primary));
} else {
// On Win/Linux Command/Meta is NOT primary
assert!(!mods.contains(&Modifier::Primary));
}
}
#[test]
fn reload_handles_missing_and_invalid_json() {
// empty (no value set) → defaults
hbb_common::config::LocalConfig::set_option(LOCAL_CONFIG_KEY.into(), String::new());
reload_from_config();
let b = current();
assert!(!b.enabled);
assert!(b.bindings.is_empty());
// invalid JSON → defaults (no panic)
hbb_common::config::LocalConfig::set_option(LOCAL_CONFIG_KEY.into(), "not json".into());
reload_from_config();
let b = current();
assert!(!b.enabled);
}
}

View File

@@ -743,20 +743,5 @@ 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", "السماح بإرساء شريط الأدوات البعيد إلى أي حافة من حواف النافذة"),
("API Token", "رمز واجهة برمجة التطبيقات API"),
("Deploy", "نشر"),
("Custom ID (optional)", "معرّف مخصص (اختياري)"),
("server_requires_deployment_tip", "يتطلب الخادم نشر هذا الجهاز بشكل صريح. هل تريد النشر الآن؟"),
("The server does not require explicit deployment.", "لا يتطلب الخادم نشرًا صريحًا."),
("Unknown response.", "استجابة غير معروفة."),
("wayland-keyboard-input-disabled-tip", "هل تريد السماح بإدخال لوحة المفاتيح؟"),
("wayland-keyboard-input-consent-tip", "ما تكتبه على هذا الكمبيوتر البعيد (بما في ذلك كلمات المرور) قد تتمكن التطبيقات الأخرى الموجودة عليه من قراءته."),
("wayland-keyboard-input-applies-to-tip", "ينطبق هذا الاختيار على:"),
("wayland-soft-keyboard-input-label", "إدخال لوحة المفاتيح الافتراضية"),
("wayland-keyboard-input-reset-choice-tip", "إعادة تعيين اختيار إدخال لوحة المفاتيح"),
("remember-wayland-keyboard-choice-tip", "لا تسأل مرة أخرى لهذا الكمبيوتر البعيد"),
("Why this happens", "سبب حدوث ذلك"),
].iter().cloned().collect();
}

View File

@@ -360,7 +360,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Запіс"),
("Directory", "Каталог"),
("Automatically record incoming sessions", "Аўтаматычна запісваць уваходныя сесіі"),
("Automatically record outgoing sessions", "Аўтаматычна запісваць выходныя сесіі"),
("Automatically record outgoing sessions", ""),
("Change", "Змяніць"),
("Start session recording", "Пачаць запіс сесіі"),
("Stop session recording", "Спыніць запіс сесіі"),
@@ -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", "Не ўдалося захапіць экран Wayland. Магчыма, XDG Desktop Portal завяршыўся аварыйна або недаступны. Паспрабуйце перазапусціць яго камандай `systemctl --user restart xdg-desktop-portal`."),
("xdp-portal-unavailable", ""),
("JumpLink", "Прагляд"),
("Please Select the screen to be shared(Operate on the peer side).", "Выберыце экран для дэманстрацыі (кіруецца на баку абанента)."),
("Show RustDesk", "Паказаць RustDesk"),
@@ -743,20 +743,5 @@ 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", "Дазволіць замацоўванне аддаленай панэлі інструментаў да любога краю акна"),
("API Token", "Токен API"),
("Deploy", "Разгарнуць"),
("Custom ID (optional)", "Карыстальніцкі ID (неабавязкова)"),
("server_requires_deployment_tip", "Сервер патрабуе яўнага разгортвання гэтай прылады. Разгарнуць зараз?"),
("The server does not require explicit deployment.", "Сервер не патрабуе яўнага разгортвання."),
("Unknown response.", "Невядомы адказ."),
("wayland-keyboard-input-disabled-tip", "Дазволіць увод з клавіятуры?"),
("wayland-keyboard-input-consent-tip", "Тое, што вы набіраеце на гэтым аддаленым кампутары (у тым ліку паролі), могуць прачытаць іншыя праграмы на ім."),
("wayland-keyboard-input-applies-to-tip", "Гэты выбар прымяняецца да:"),
("wayland-soft-keyboard-input-label", "Увод з экраннай клавіятуры"),
("wayland-keyboard-input-reset-choice-tip", "Скінуць выбар уводу з клавіятуры"),
("remember-wayland-keyboard-choice-tip", "Не пытацца зноў для гэтага аддаленага кампутара"),
("Why this happens", "Чаму гэта адбываецца"),
].iter().cloned().collect();
}

View File

@@ -303,7 +303,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Ignore Battery Optimizations", "Игнорирай оптимизациите на батерията"),
("android_open_battery_optimizations_tip", "Ако искате да деактивирате тази функция, моля, отидете на следващата страница с настройки на приложението RustDesk, намерете и въведете [Battery], премахнете отметката от [Unrestricted]"),
("Start on boot", "Стартирайте при зареждане"),
("Start the screen sharing service on boot, requires special permissions", "Стартиране на услугата за споделяне на екрана при зареждане, изисква специални разрешения"),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", "Връзката непозволена"),
("Legacy mode", "По остарял начин"),
("Map mode", "По начин със съответствие (map)"),
@@ -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", "Заснемането на екрана под Wayland е неуспешно. XDG Desktop Portal може да е блокирал или да е недостъпен. Опитайте да го рестартирате с `systemctl --user restart xdg-desktop-portal`."),
("xdp-portal-unavailable", ""),
("JumpLink", "Препратка"),
("Please Select the screen to be shared(Operate on the peer side).", "Моля, изберете екрана, който да бъде споделен (спрямо отдалечената страна)."),
("Show RustDesk", "Покажи RustDesk"),
@@ -557,7 +557,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("List", "Списък"),
("Virtual display", "Виртуален екран"),
("Plug out all", "Разкачане на всички"),
("True color (4:4:4)", "Истински цвят (4:4:4)"),
("True color (4:4:4)", ""),
("Enable blocking user input", "Разрешаване на блокиране на потребителско въвеждане"),
("id_input_tip", "Можете да въведете ID, директен IP адрес или домейн с порт (<domain>:<port>).\nАко искате да получите достъп до устройство на друг сървър, моля, добавете адреса на сървъра (<id>@<server_address >?key=<key_value>), например\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nАко искате да получите достъп до устройство на обществен сървър, моля, въведете \"<id>@public\" , ключът не е необходим за публичен сървър"),
("privacy_mode_impl_mag_tip", "Режим 1"),
@@ -567,7 +567,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("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 control-command key", ""),
("swap-left-right-mouse", "Размяна на копчетата на мишката"),
("2FA code", "Код за Двуфакторно удостоверяване"),
("More", "Повече"),
@@ -579,9 +579,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("2FA code must be 6 digits.", "Кодът за 2FA (двуфакторно удостоверяване) трябва да е 6-цифрен"),
("Multiple Windows sessions found", "Установени са няколко Windwos сесии"),
("Please select the session you want to connect to", "Моля определете сесия към която искате да се свърженете"),
("powered_by_me", "Работи с RustDesk"),
("outgoing_only_desk_tip", "Това е персонализирано издание.\nМожете да се свързвате с други устройства, но други устройства не могат да се свързват с вашето устройство."),
("preset_password_warning", "Това персонализирано издание идва с предварително зададена парола. Всеки, който знае тази парола, може да получи пълен контрол върху вашето устройство. Ако не сте очаквали това, незабавно деинсталирайте софтуера."),
("powered_by_me", ""),
("outgoing_only_desk_tip", ""),
("preset_password_warning", ""),
("Security Alert", "Предупреждение за сигурност"),
("My address book", "Моята адресна книга"),
("Personal", "Личен"),
@@ -591,25 +591,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Read-only", "Само четене"),
("Read/Write", "Писане/четене"),
("Full Control", "Пълен контрол"),
("share_warning_tip", "Полетата по-горе са споделени и видими за други хора."),
("share_warning_tip", ""),
("Everyone", "Всички"),
("ab_web_console_tip", "Повече в уеб конзолата"),
("allow-only-conn-window-open-tip", "Разрешаване на връзка само ако прозорецът на RustDesk е отворен"),
("no_need_privacy_mode_no_physical_displays_tip", "Няма физически екрани, не е необходимо да се използва режимът на поверителност."),
("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", "Протоколът и портът по подразбиране са Socks5 и 1080"),
("no_audio_input_device_tip", "Не е намерено устройство за аудио вход."),
("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", "Сигурни ли сте, че искате да изчистите избора на екран за Wayland?"),
("android_new_voice_call_tip", "Получена е нова заявка за гласово обаждане. Ако приемете, звукът ще премине към гласова комуникация."),
("texture_render_tip", "Използвайте рендер на текстури, за да направите картината по-плавна. Можете да опитате да изключите тази опция, ако срещнете проблеми с изобразяването."),
("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"),
("floating_window_tip", ""),
("Keep screen on", "Запази екранът включен"),
("Never", "Никога"),
("During controlled", "Докато е обект на управление"),
@@ -621,142 +621,127 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Volume down", "Намаляване звук"),
("Power", "Мощност"),
("Telegram bot", "Телеграм бот"),
("enable-bot-tip", "Ако активирате тази функция, можете да получавате 2FA кода от вашия бот. Той може да функционира и като известие за връзка."),
("enable-bot-desc", "1. Отворете чат с @BotFather.\n2. Изпратете командата \"/newbot\". След като завършите тази стъпка, ще получите токен.\n3. Започнете чат с новосъздадения си бот. Изпратете съобщение, започващо с наклонена черта (\"/\"), например \"/hello\", за да го активирате.\n"),
("cancel-2fa-confirm-tip", "Сигурни ли сте, че искате да отмените 2FA?"),
("cancel-bot-confirm-tip", "Сигурни ли сте, че искате да отмените бота на Telegram?"),
("enable-bot-tip", ""),
("enable-bot-desc", ""),
("cancel-2fa-confirm-tip", ""),
("cancel-bot-confirm-tip", ""),
("About RustDesk", "За RustDesk"),
("Send clipboard keystrokes", "Изпращане на клавишни натискания от клипборда"),
("network_error_tip", "Моля, проверете мрежовата си връзка, след което натиснете повторен опит."),
("Send clipboard keystrokes", ""),
("network_error_tip", ""),
("Unlock with PIN", "Отключване с PIN"),
("Requires at least {} characters", "Изисква поне {} знака"),
("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", "Родителска папка"),
("enable-trusted-devices-tip", ""),
("Parent directory", ""),
("Resume", "Възобновяване"),
("Invalid file name", "Невалидно име за файл"),
("one-way-file-transfer-tip", "Еднопосочното прехвърляне на файлове е активирано от управляваната страна."),
("Authentication Required", "Изисква се удостоверяване"),
("Authenticate", "Удостоверяване"),
("web_id_input_tip", "Можете да въведете ID на същия сървър, директният достъп по IP не се поддържа в уеб клиента.\nАко искате да получите достъп до устройство на друг сървър, моля, добавете адреса на сървъра (<id>@<server_address>?key=<key_value>), например,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nАко искате да получите достъп до устройство на публичен сървър, моля, въведете \"<id>@public\", за публичен сървър не е необходим ключ."),
("Download", "Изтегляне"),
("Upload folder", "Качване на папка"),
("Upload files", "Качване на файлове"),
("Clipboard is synchronized", "Клипбордът е синхронизиран"),
("Update client clipboard", "Обновяване на клипборда на клиента"),
("Untagged", "Без етикет"),
("new-version-of-{}-tip", "Налична е нова версия на {}"),
("Accessible devices", "Достъпни устройства"),
("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 рендерът, екранът за отдалечено управление може да е черен на някои машини."),
("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"),
("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", "Въведете username или domain\\username"),
("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", "ЗАБЕЛЕЖКА: RustDesk server 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", "Списък с промени"),
("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", ""),
("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", ""),
("Continue with {}", "Продължи с {}"),
("Display Name", "Показвано име"),
("password-hidden-tip", "Зададена е постоянна парола (скрита)."),
("preset-password-in-use-tip", "В момента се използва предварително зададена парола."),
("Enable privacy mode", "Позволяване на режим на поверителност"),
("allow-remote-toolbar-docking-any-edge", "Позволяване на закачане на отдалечената лента с инструменти към всеки ръб на прозореца"),
("API Token", "API токен"),
("Deploy", "Внедряване"),
("Custom ID (optional)", "Персонализиран ID (по избор)"),
("server_requires_deployment_tip", "Сървърът изисква това устройство да бъде внедрено изрично. Да се внедри ли сега?"),
("The server does not require explicit deployment.", "Сървърът не изисква изрично внедряване."),
("Unknown response.", "Неизвестен отговор."),
("wayland-keyboard-input-disabled-tip", "Да се позволи ли въвеждане от клавиатура?"),
("wayland-keyboard-input-consent-tip", "Това, което въвеждате на този отдалечен компютър (включително пароли), може да бъде прочетено от други приложения на него."),
("wayland-keyboard-input-applies-to-tip", "Този избор се отнася за:"),
("wayland-soft-keyboard-input-label", "Въвеждане от софтуерна клавиатура"),
("wayland-keyboard-input-reset-choice-tip", "Нулиране на избора за въвеждане от клавиатура"),
("remember-wayland-keyboard-choice-tip", "Не питай отново за този отдалечен компютър"),
("Why this happens", "Защо се случва това"),
("Display Name", ""),
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
].iter().cloned().collect();
}

View File

@@ -360,7 +360,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Gravació"),
("Directory", "Contactes"),
("Automatically record incoming sessions", "Enregistrament automàtic de sessions entrants"),
("Automatically record outgoing sessions", "Enregistrament automàtic de sessions sortints"),
("Automatically record outgoing sessions", ""),
("Change", "Canvia"),
("Start session recording", "Inicia la gravació de la sessió"),
("Stop session recording", "Atura la gravació de la sessió"),
@@ -379,7 +379,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Screen Share", "Compartició de pantalla"),
("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", "Ha fallat la captura de pantalla del Wayland. És possible que el XDG Desktop Portal hagi fallat o no estigui disponible. Proveu de reiniciar-lo amb `systemctl --user restart xdg-desktop-portal`."),
("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"),
@@ -650,43 +650,43 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Clipboard is synchronized", "El porta-retalls està sincronitzat"),
("Update client clipboard", "Actualitza el porta-retalls del client"),
("Untagged", "Sense etiquetar"),
("new-version-of-{}-tip", "Hi ha disponible una versió nova de {}"),
("new-version-of-{}-tip", ""),
("Accessible devices", "Dispositius accessibles"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Actualitzeu el client RustDesk a la versió {} o superior a la part remota!"),
("d3d_render_tip", "Quan la renderització D3D està habilitada, en alguns equips la pantalla del control remot pot quedar en negre."),
("upgrade_remote_rustdesk_client_to_{}_tip", ""),
("d3d_render_tip", ""),
("Use D3D rendering", "Utilitza renderització D3D"),
("Printer", "Impressora"),
("printer-os-requirement-tip", "La funció d'impressió sortint requereix Windows 10 o superior."),
("printer-requires-installed-{}-client-tip", "Per a utilitzar la impressió remota, cal instal·lar {} en aquest dispositiu."),
("printer-{}-not-installed-tip", "La impressora {} no està instal·lada."),
("printer-{}-ready-tip", "La impressora {} està instal·lada i a punt per a utilitzar-se."),
("printer-os-requirement-tip", ""),
("printer-requires-installed-{}-client-tip", ""),
("printer-{}-not-installed-tip", ""),
("printer-{}-ready-tip", ""),
("Install {} Printer", "Instal·la {} impressora"),
("Outgoing Print Jobs", "Treballs d'impressió sortints"),
("Incoming Print Jobs", "Treballs d'impressió entrants"),
("Incoming Print Job", "Treballs d'impressió entrant"),
("use-the-default-printer-tip", "Utilitza la impressora per defecte"),
("use-the-selected-printer-tip", "Utilitza la impressora seleccionada"),
("auto-print-tip", "Imprimeix automàticament utilitzant la impressora seleccionada."),
("print-incoming-job-confirm-tip", "Heu rebut un treball d'impressió des de la part remota. Voleu executar-lo al vostre costat?"),
("remote-printing-disallowed-tile-tip", "Impressió remota no permesa"),
("remote-printing-disallowed-text-tip", "La configuració de permisos de la part controlada denega la impressió remota."),
("save-settings-tip", "Desa la configuració"),
("dont-show-again-tip", "No tornis a mostrar això"),
("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", "Fes una captura de pantalla"),
("Taking screenshot", "Fent la captura de pantalla"),
("screenshot-merged-screen-not-supported-tip", "Actualment no és possible combinar captures de pantalla de diverses pantalles. Canvieu a una sola pantalla i torneu a provar."),
("screenshot-action-tip", "Seleccioneu com voleu continuar amb la captura de pantalla."),
("screenshot-merged-screen-not-supported-tip", ""),
("screenshot-action-tip", ""),
("Save as", "Anomena i desa"),
("Copy to clipboard", "Copia al porta-retalls"),
("Enable remote printer", "Habilita l'impressora remota"),
("Downloading {}", "Descarregant {}"),
("{} Update", "{} Actualitza"),
("{}-to-update-tip", "{} es tancarà ara i instal·larà la versió nova."),
("download-new-version-failed-tip", "Ha fallat la descàrrega. Podeu tornar a provar o fer clic al botó \"Descarrega\" per descarregar-la des de la pàgina de publicacions i actualitzar-la manualment."),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", "Actualització automàtica"),
("update-failed-check-msi-tip", "Ha fallat la comprovació del mètode d'instal·lació. Feu clic al botó \"Descarrega\" per descarregar-la des de la pàgina de publicacions i actualitzar-la manualment."),
("websocket_tip", "En utilitzar WebSocket, només s'admeten connexions per repetidor."),
("Use WebSocket", "Utilitza WebSocket"),
("update-failed-check-msi-tip", ""),
("websocket_tip", ""),
("Use WebSocket", ""),
("Trackpad speed", "Velocitat del trackpad"),
("Default trackpad speed", "Velocitat per defecte del trackpad"),
("Numeric one-time password", "Contrasenya numèrica d'un sol ús"),
@@ -695,19 +695,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("View camera", "Mostra la càmera"),
("Enable camera", "Habilita la càmera"),
("No cameras", "No hi ha càmeres"),
("view_camera_unsupported_tip", "El dispositiu remot no admet la visualització de la càmera."),
("view_camera_unsupported_tip", ""),
("Terminal", "Terminal"),
("Enable terminal", "Habilita el terminal"),
("New tab", "Nova finestra"),
("Keep terminal sessions on disconnect", "Mantingues les sessions de terminal desconnectades"),
("Terminal (Run as administrator)", "Terminal (executa com a administrador"),
("terminal-admin-login-tip", "Inseriu el nom d'usuari i la contrasenya de l'administrador de la part controlada."),
("terminal-admin-login-tip", ""),
("Failed to get user token.", "No s'ha pogut obtenir el token d'usuari."),
("Incorrect username or password.", "Nom d'usuari o contrasenya incorrecte"),
("The user is not an administrator.", "Aquest usuari no és administrador"),
("Failed to check if the user is an administrator.", "No s'ha pogut comprovar si l'usuari és administrador."),
("Supported only in the installed version.", "Només compatible amb la versió instal·lada."),
("elevation_username_tip", "Inseriu el nom d'usuari o domini\\nomusuari"),
("elevation_username_tip", ""),
("Preparing for installation ...", "Preparant per a l'instal·lació..."),
("Show my cursor", "Mostra el meu punter"),
("Scale custom", "Escala personalitzada"),
@@ -721,42 +721,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", "Mostra el joystick virtual"),
("Edit note", "Edita la nota"),
("Alias", "Alias"),
("ScrollEdge", "Desplaçament a la vora"),
("Allow insecure TLS fallback", "Permet l'ús alternatiu de TLS no segur"),
("allow-insecure-tls-fallback-tip", "Per defecte, el RustDesk verifica el certificat del servidor per als protocols que utilitzen TLS.\nAmb aquesta opció habilitada, el RustDesk ometrà el pas de verificació i continuarà en cas que aquesta falli."),
("Disable UDP", "Inhabilita l'UDP"),
("disable-udp-tip", "Controla si s'utilitza només TCP.\nAmb aquesta opció habilitada, el RustDesk ja no utilitzarà l'UDP 21116, sinó que utilitzarà el TCP 21116 en el seu lloc."),
("server-oss-not-support-tip", "NOTA: El RustDesk Server OSS no inclou aquesta característica."),
("input note here", "inseriu la nota aquí"),
("note-at-conn-end-tip", "Demana una nota en finalitzar la connexió"),
("Show terminal extra keys", "Mostra les tecles addicionals del terminal"),
("Relative mouse mode", "Mode de ratolí relatiu"),
("rel-mouse-not-supported-peer-tip", "El client connectat no admet el mode de ratolí relatiu."),
("rel-mouse-not-ready-tip", "El mode de ratolí relatiu encara no està a punt. Torneu a provar."),
("rel-mouse-lock-failed-tip", "Ha fallat el bloqueig del cursor. S'ha inhabilitat el mode de ratolí relatiu."),
("rel-mouse-exit-{}-tip", "Premeu {} per a sortir."),
("rel-mouse-permission-lost-tip", "S'ha revocat el permís del teclat. S'ha inhabilitat el mode de ratolí relatiu."),
("Changelog", "Registre de canvis"),
("keep-awake-during-outgoing-sessions-label", "Mantén la pantalla activa durant les sessions sortints"),
("keep-awake-during-incoming-sessions-label", "Mantén la pantalla activa durant les sessions entrants"),
("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", ""),
("Continue with {}", "Continua amb {}"),
("Display Name", "Nom visible"),
("password-hidden-tip", "La contrasenya permanent està definida (oculta)."),
("preset-password-in-use-tip", "Actualment s'està utilitzant una contrasenya preestablerta."),
("Enable privacy mode", "Habilita el Mode privat"),
("allow-remote-toolbar-docking-any-edge", "Permet ancorar la barra d'eines remota a qualsevol vora de la finestra"),
("API Token", "Testimoni de l'API"),
("Deploy", "Desplega"),
("Custom ID (optional)", "ID personalitzada (opcional)"),
("server_requires_deployment_tip", "El servidor requereix que aquest dispositiu es desplegui explícitament. Voleu desplegar-lo ara?"),
("The server does not require explicit deployment.", "El servidor no requereix un desplegament explícit."),
("Unknown response.", "Resposta desconeguda."),
("wayland-keyboard-input-disabled-tip", "Voleu permetre l'entrada de teclat?"),
("wayland-keyboard-input-consent-tip", "Allò que escriviu en aquest equip remot (incloses les contrasenyes) podria ser llegit per altres aplicacions que hi hagi."),
("wayland-keyboard-input-applies-to-tip", "Aquesta opció s'aplica a:"),
("wayland-soft-keyboard-input-label", "Entrada de teclat virtual"),
("wayland-keyboard-input-reset-choice-tip", "Restableix l'opció d'entrada de teclat"),
("remember-wayland-keyboard-choice-tip", "No tornis a preguntar-ho per a aquest equip remot"),
("Why this happens", "Per què passa això"),
("Display Name", ""),
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
].iter().cloned().collect();
}

View File

@@ -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", "Wayland 屏幕捕获失败。XDG Desktop Portal 可能已崩溃或不可用。请尝试使用 `systemctl --user restart xdg-desktop-portal` 重启它。"),
("xdp-portal-unavailable", ""),
("JumpLink", "查看"),
("Please Select the screen to be shared(Operate on the peer side).", "请选择要分享的画面(对端操作)。"),
("Show RustDesk", "显示 RustDesk"),
@@ -743,20 +743,44 @@ 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", "允许将远程工具栏停靠到任意窗口边缘"),
("API Token", "API 令牌"),
("Deploy", "部署"),
("Custom ID (optional)", "自定义 ID可选"),
("server_requires_deployment_tip", "服务器要求显式部署此设备。是否立即部署"),
("The server does not require explicit deployment.", "服务器不需要显式部署。"),
("Unknown response.", "未知响应。"),
("wayland-keyboard-input-disabled-tip", "允许键盘输入?"),
("wayland-keyboard-input-consent-tip", "你在这台远程电脑上输入的内容(包括密码)可能被远程电脑上的其他程序读取。"),
("wayland-keyboard-input-applies-to-tip", "此选择适用于:"),
("wayland-soft-keyboard-input-label", "软键盘输入"),
("wayland-keyboard-input-reset-choice-tip", "重置键盘输入选择"),
("remember-wayland-keyboard-choice-tip", "以后对这台远程电脑不再询问"),
("Why this happens", "了解原因"),
("Keyboard Shortcuts", "键盘快捷键"),
("Configure shortcuts...", "配置快捷键..."),
("Enable keyboard shortcuts in remote session", "在远程会话中启用键盘快捷键"),
("shortcut-page-description", "启用后,列出的组合键将在本地触发会话操作,而不会发送到远程端。所有快捷键必须包含 Ctrl+Alt+ShiftmacOS 上为 Cmd+Option+Shift以避免与正常输入冲突。"),
("Reset to defaults", "恢复默认设置"),
("shortcut-reset-confirm-tip", "这将以默认快捷键替换所有当前绑定。是否继续"),
("Session Control", "会话控制"),
("Toggle Fullscreen", "切换全屏"),
("Switch to next display", "切换到下一个显示器"),
("Switch to previous display", "切换到上一个显示器"),
("View Mode 1:1", "原始大小"),
("View Mode Shrink", "缩小"),
("View Mode Stretch", "拉伸"),
("Take Screenshot", "截图"),
("Toggle Audio", "切换音频"),
("Toggle Privacy Mode", "切换隐私模式"),
("Toggle Recording", "切换录制"),
("Toggle Block User Input", "切换屏蔽用户输入"),
("Switch Tab 1", "切换到第 1 个标签"),
("Switch Tab 2", "切换到第 2 个标签"),
("Switch Tab 3", "切换到第 3 个标签"),
("Switch Tab 4", "切换到第 4 个标签"),
("Switch Tab 5", "切换到第 5 个标签"),
("Switch Tab 6", "切换到第 6 个标签"),
("Switch Tab 7", "切换到第 7 个标签"),
("Switch Tab 8", "切换到第 8 个标签"),
("Switch Tab 9", "切换到第 9 个标签"),
("Edit", "编辑"),
("Save", "保存"),
("Set Shortcut", "设置快捷键"),
("shortcut-recording-instruction", "请按下您想使用的组合键。"),
("shortcut-recording-press-keys-tip", "请按下组合键..."),
("shortcut-must-include-prefix", "必须包含"),
("shortcut-already-bound-to", "已绑定到"),
("Replace", "替换"),
("Valid", "有效"),
("shortcut-mobile-physical-keyboard-tip", "录制需要使用物理键盘,不支持软键盘。"),
("On", ""),
("Off", ""),
].iter().cloned().collect();
}

View File

@@ -360,7 +360,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Nahrávání"),
("Directory", "Adresář"),
("Automatically record incoming sessions", "Automaticky nahrávat příchozí relace"),
("Automatically record outgoing sessions", "Automaticky nahrávat odchozí relace"),
("Automatically record outgoing sessions", ""),
("Change", "Změnit"),
("Start session recording", "Spustit záznam relace"),
("Stop session recording", "Zastavit záznam relace"),
@@ -379,7 +379,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Screen Share", "Sdílení obrazovky"),
("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", "Záznam obrazovky ve Waylandu selhal. XDG Desktop Portal mohl spadnout nebo je nedostupný. Zkuste jej restartovat příkazem `systemctl --user restart xdg-desktop-portal`."),
("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"),
@@ -640,123 +640,108 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Parent directory", "Rodičovský adresář"),
("Resume", "Pokračovat"),
("Invalid file name", "Nesprávný název souboru"),
("one-way-file-transfer-tip", "Na ovládané straně je povolen jednosměrný přenos souborů."),
("Authentication Required", "Vyžadováno ověření"),
("Authenticate", "Ověřit"),
("web_id_input_tip", "Můžete zadat ID na stejném serveru, přímý přístup přes IP není ve webovém klientovi podporován.\nPokud chcete přistupovat k zařízení na jiném serveru, připojte adresu serveru (<id>@<server_address>?key=<key_value>), například,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nPokud chcete přistupovat k zařízení na veřejném serveru, zadejte \"<id>@public\", pro veřejný server není klíč potřeba."),
("Download", "Stáhnout"),
("Upload folder", "Nahrát složku"),
("Upload files", "Nahrát soubory"),
("Clipboard is synchronized", "Schránka je synchronizována"),
("Update client clipboard", "Aktualizovat schránku klienta"),
("Untagged", "Bez štítku"),
("new-version-of-{}-tip", "Je k dispozici nová verze {}"),
("Accessible devices", "Přístupná zařízení"),
("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", "Upgradujte prosím klienta RustDesk na verzi {} nebo novější na vzdálené straně!"),
("d3d_render_tip", "Když je povoleno vykreslování D3D, může být obrazovka vzdáleného ovládání na některých počítačích černá."),
("Use D3D rendering", "Použít vykreslování D3D"),
("Printer", "Tiskárna"),
("printer-os-requirement-tip", "Funkce odchozího tisku vyžaduje Windows 10 nebo novější."),
("printer-requires-installed-{}-client-tip", "Aby bylo možné používat vzdálený tisk, musí být na tomto zařízení nainstalován {}."),
("printer-{}-not-installed-tip", "Tiskárna {} není nainstalována."),
("printer-{}-ready-tip", "Tiskárna {} je nainstalována a připravena k použití."),
("Install {} Printer", "Nainstalovat tiskárnu {}"),
("Outgoing Print Jobs", "Odchozí tiskové úlohy"),
("Incoming Print Jobs", "Příchozí tiskové úlohy"),
("Incoming Print Job", "Příchozí tisková úloha"),
("use-the-default-printer-tip", "Použít výchozí tiskárnu"),
("use-the-selected-printer-tip", "Použít vybranou tiskárnu"),
("auto-print-tip", "Tisknout automaticky pomocí vybrané tiskárny."),
("print-incoming-job-confirm-tip", "Obdrželi jste tiskovou úlohu ze vzdáleného počítače. Chcete ji provést na své straně?"),
("remote-printing-disallowed-tile-tip", "Vzdálený tisk není povolen"),
("remote-printing-disallowed-text-tip", "Nastavení oprávnění ovládané strany zakazuje vzdálený tisk."),
("save-settings-tip", "Uložit nastavení"),
("dont-show-again-tip", "Toto již nezobrazovat"),
("Take screenshot", "Pořídit snímek obrazovky"),
("Taking screenshot", "Pořizuje se snímek obrazovky"),
("screenshot-merged-screen-not-supported-tip", "Sloučení snímků obrazovky z více displejů aktuálně není podporováno. Přepněte na jeden displej a zkuste to znovu."),
("screenshot-action-tip", "Vyberte, jak pokračovat se snímkem obrazovky."),
("Save as", "Uložit jako"),
("Copy to clipboard", "Kopírovat do schránky"),
("Enable remote printer", "Povolit vzdálenou tiskárnu"),
("Downloading {}", "Stahuje se {}"),
("{} Update", "Aktualizace {}"),
("{}-to-update-tip", "{} se nyní zavře a nainstaluje novou verzi."),
("download-new-version-failed-tip", "Stahování se nezdařilo. Můžete to zkusit znovu nebo kliknout na tlačítko \"Stáhnout\" pro stažení ze stránky vydání a ruční aktualizaci."),
("Auto update", "Automatická aktualizace"),
("update-failed-check-msi-tip", "Kontrola metody instalace se nezdařila. Klikněte na tlačítko \"Stáhnout\" pro stažení ze stránky vydání a ruční aktualizaci."),
("websocket_tip", "Při použití WebSocket jsou podporována pouze přenosová (relay) připojení."),
("Use WebSocket", "Použít WebSocket"),
("Trackpad speed", "Rychlost trackpadu"),
("Default trackpad speed", "Výchozí rychlost trackpadu"),
("Numeric one-time password", "Číselné jednorázové heslo"),
("Enable IPv6 P2P connection", "Povolit připojení IPv6 P2P"),
("Enable UDP hole punching", "Povolit UDP hole punching"),
("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", "Zobrazit kameru"),
("Enable camera", "Povolit kameru"),
("No cameras", "Žádné kamery"),
("view_camera_unsupported_tip", "Vzdálené zařízení nepodporuje zobrazení kamery."),
("Terminal", "Terminál"),
("Enable terminal", "Povolit terminál"),
("New tab", "Nová karta"),
("Keep terminal sessions on disconnect", "Zachovat relace terminálu při odpojení"),
("Terminal (Run as administrator)", "Terminál (Spustit jako správce)"),
("terminal-admin-login-tip", "Zadejte uživatelské jméno a heslo správce ovládané strany."),
("Failed to get user token.", "Nepodařilo se získat uživatelský token."),
("Incorrect username or password.", "Nesprávné uživatelské jméno nebo heslo."),
("The user is not an administrator.", "Uživatel není správce."),
("Failed to check if the user is an administrator.", "Nepodařilo se ověřit, zda je uživatel správce."),
("Supported only in the installed version.", "Podporováno pouze v nainstalované verzi."),
("elevation_username_tip", "Zadejte uživatelské jméno nebo doména\\uživatelské jméno"),
("Preparing for installation ...", "Příprava instalace ..."),
("Show my cursor", "Zobrazit můj kurzor"),
("Scale custom", "Vlastní měřítko"),
("Custom scale slider", "Posuvník vlastního měřítka"),
("Decrease", "Zmenšit"),
("Increase", "Zvětšit"),
("Show virtual mouse", "Zobrazit virtuální myš"),
("Virtual mouse size", "Velikost virtuální myši"),
("Small", "Malá"),
("Large", "Velká"),
("Show virtual joystick", "Zobrazit virtuální joystick"),
("Edit note", "Upravit poznámku"),
("Alias", "Alias"),
("ScrollEdge", "ScrollEdge"),
("Allow insecure TLS fallback", "Povolit nezabezpečené záložní řešení TLS"),
("allow-insecure-tls-fallback-tip", "Ve výchozím nastavení RustDesk ověřuje certifikát serveru u protokolů používajících TLS.\nKdyž je tato možnost povolena, RustDesk v případě selhání ověření přejde k přeskočení kroku ověření a bude pokračovat."),
("Disable UDP", "Zakázat UDP"),
("disable-udp-tip", "Určuje, zda se má používat pouze TCP.\nKdyž je tato možnost povolena, RustDesk již nebude používat UDP 21116, místo toho se použije TCP 21116."),
("server-oss-not-support-tip", "POZNÁMKA: RustDesk server OSS tuto funkci neobsahuje."),
("input note here", "sem zadejte poznámku"),
("note-at-conn-end-tip", "Požádat o poznámku na konci připojení"),
("Show terminal extra keys", "Zobrazit další klávesy terminálu"),
("Relative mouse mode", "Relativní režim myši"),
("rel-mouse-not-supported-peer-tip", "Připojený protějšek nepodporuje relativní režim myši."),
("rel-mouse-not-ready-tip", "Relativní režim myši ještě není připraven. Zkuste to znovu."),
("rel-mouse-lock-failed-tip", "Nepodařilo se uzamknout kurzor. Relativní režim myši byl zakázán."),
("rel-mouse-exit-{}-tip", "Stiskněte {} pro ukončení."),
("rel-mouse-permission-lost-tip", "Oprávnění ke klávesnici bylo odebráno. Relativní režim myši byl zakázán."),
("Changelog", "Seznam změn"),
("keep-awake-during-outgoing-sessions-label", "Udržovat obrazovku aktivní během odchozích relací"),
("keep-awake-during-incoming-sessions-label", "Udržovat obrazovku aktivní během příchozích relací"),
("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", ""),
("Continue with {}", "Pokračovat s {}"),
("Display Name", "Zobrazované jméno"),
("password-hidden-tip", "Trvalé heslo je nastaveno (skryto)."),
("preset-password-in-use-tip", "Aktuálně se používá přednastavené heslo."),
("Enable privacy mode", "Povolit režim ochrany soukromí"),
("allow-remote-toolbar-docking-any-edge", "Povolit ukotvení vzdáleného panelu nástrojů k libovolnému okraji okna"),
("API Token", "API token"),
("Deploy", "Nasadit"),
("Custom ID (optional)", "Vlastní ID (volitelné)"),
("server_requires_deployment_tip", "Server vyžaduje, aby bylo toto zařízení výslovně nasazeno. Nasadit nyní?"),
("The server does not require explicit deployment.", "Server nevyžaduje výslovné nasazení."),
("Unknown response.", "Neznámá odpověď."),
("wayland-keyboard-input-disabled-tip", "Povolit vstup z klávesnice?"),
("wayland-keyboard-input-consent-tip", "To, co píšete na tomto vzdáleném počítači (včetně hesel), mohou číst jiné aplikace na něm."),
("wayland-keyboard-input-applies-to-tip", "Tato volba platí pro:"),
("wayland-soft-keyboard-input-label", "Vstup ze softwarové klávesnice"),
("wayland-keyboard-input-reset-choice-tip", "Resetovat volbu vstupu z klávesnice"),
("remember-wayland-keyboard-choice-tip", "Pro tento vzdálený počítač se již neptat"),
("Why this happens", "Proč k tomu dochází"),
("Display Name", ""),
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
].iter().cloned().collect();
}

View File

@@ -360,7 +360,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Optager"),
("Directory", "Mappe"),
("Automatically record incoming sessions", "Optag automatisk indgående sessioner"),
("Automatically record outgoing sessions", "Optag automatisk udgående sessioner"),
("Automatically record outgoing sessions", ""),
("Change", "Ændr"),
("Start session recording", "Start sessionsoptagelse"),
("Stop session recording", "Stop sessionsoptagelse"),
@@ -379,7 +379,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Screen Share", "Skærmdeling"),
("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", "Skærmoptagelse via Wayland mislykkedes. XDG Desktop Portal kan være gået ned eller er utilgængelig. Prøv at genstarte den med `systemctl --user restart xdg-desktop-portal`."),
("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"),
@@ -640,123 +640,108 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Parent directory", "mappe"),
("Resume", "Fortsæt"),
("Invalid file name", "Ugyldigt filnavn"),
("one-way-file-transfer-tip", "Envejs-filoverførsel er aktiveret på den kontrollerede side."),
("Authentication Required", "Godkendelse påkrævet"),
("Authenticate", "Godkend"),
("web_id_input_tip", "Du kan indtaste et ID på den samme server; direkte IP-adgang understøttes ikke i webklienten.\nHvis du ønsker at få adgang til en enhed på en anden server, tilføj da serveradressen (<id>@<server_address>?key=<key_value>), fx,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHvis du ønsker at få adgang til en enhed på en offentlig server, indtast da \"<id>@public\"; nøglen er ikke nødvendig for offentlige servere."),
("Download", "Download"),
("Upload folder", "Upload mappe"),
("Upload files", "Upload filer"),
("Clipboard is synchronized", "Udklipsholderen er synkroniseret"),
("Update client clipboard", "Opdatér klientens udklipsholder"),
("Untagged", "Uden nøgleord"),
("new-version-of-{}-tip", "Der findes en ny version af {}"),
("Accessible devices", "Tilgængelige enheder"),
("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", "Opgrader venligst RustDesk-klienten til version {} eller nyere på fjernsiden!"),
("d3d_render_tip", "Når D3D-rendering er aktiveret, kan fjernstyringsskærmen være sort på nogle maskiner."),
("Use D3D rendering", "Anvend D3D-rendering"),
("Printer", "Printer"),
("printer-os-requirement-tip", "Den udgående printerfunktion kræver Windows 10 eller nyere."),
("printer-requires-installed-{}-client-tip", "For at kunne bruge fjernudskrivning skal {} være installeret på denne enhed."),
("printer-{}-not-installed-tip", "{}-printeren er ikke installeret."),
("printer-{}-ready-tip", "{}-printeren er installeret og klar til brug."),
("Install {} Printer", "Installér {}-printer"),
("Outgoing Print Jobs", "Udgående udskriftsjob"),
("Incoming Print Jobs", "Indgående udskriftsjob"),
("Incoming Print Job", "Indgående udskriftsjob"),
("use-the-default-printer-tip", "Brug standardprinteren"),
("use-the-selected-printer-tip", "Brug den valgte printer"),
("auto-print-tip", "Udskriv automatisk med den valgte printer."),
("print-incoming-job-confirm-tip", "Du har modtaget et udskriftsjob fra fjernenheden. Vil du udføre det på din side?"),
("remote-printing-disallowed-tile-tip", "Fjernudskrivning ikke tilladt"),
("remote-printing-disallowed-text-tip", "Tilladelsesindstillingerne på den kontrollerede side afviser fjernudskrivning."),
("save-settings-tip", "Gem indstillinger"),
("dont-show-again-tip", "Vis ikke dette igen"),
("Take screenshot", "Tag skærmbillede"),
("Taking screenshot", "Tager skærmbillede"),
("screenshot-merged-screen-not-supported-tip", "Sammenfletning af skærmbilleder fra flere skærme understøttes ikke i øjeblikket. Skift venligst til en enkelt skærm og prøv igen."),
("screenshot-action-tip", "Vælg venligst, hvordan du vil fortsætte med skærmbilledet."),
("Save as", "Gem som"),
("Copy to clipboard", "Kopiér til udklipsholder"),
("Enable remote printer", "Aktivér fjernprinter"),
("Downloading {}", "Downloader {}"),
("{} Update", "{}-opdatering"),
("{}-to-update-tip", "{} lukker nu og installerer den nye version."),
("download-new-version-failed-tip", "Download mislykkedes. Du kan prøve igen eller klikke på knappen \"Download\" for at hente fra udgivelsessiden og opgradere manuelt."),
("Auto update", "Automatisk opdatering"),
("update-failed-check-msi-tip", "Kontrol af installationsmetode mislykkedes. Klik venligst på knappen \"Download\" for at hente fra udgivelsessiden og opgradere manuelt."),
("websocket_tip", "Ved brug af WebSocket understøttes kun relay-forbindelser."),
("Use WebSocket", "Brug WebSocket"),
("Trackpad speed", "Pegefeltshastighed"),
("Default trackpad speed", "Standard pegefeltshastighed"),
("Numeric one-time password", "Numerisk engangskode"),
("Enable IPv6 P2P connection", "Aktivér IPv6 P2P-forbindelse"),
("Enable UDP hole punching", "Aktivér UDP hole punching"),
("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", "Se kamera"),
("Enable camera", "Aktivér kamera"),
("No cameras", "Ingen kameraer"),
("view_camera_unsupported_tip", "Fjernenheden understøtter ikke visning af kameraet."),
("Terminal", "Terminal"),
("Enable terminal", "Aktivér terminal"),
("New tab", "Ny fane"),
("Keep terminal sessions on disconnect", "Behold terminalsessioner ved afbrydelse"),
("Terminal (Run as administrator)", "Terminal (Kør som administrator)"),
("terminal-admin-login-tip", "Indtast venligst administratorbrugernavnet og adgangskoden på den kontrollerede side."),
("Failed to get user token.", "Kunne ikke hente brugertoken."),
("Incorrect username or password.", "Forkert brugernavn eller adgangskode."),
("The user is not an administrator.", "Brugeren er ikke administrator."),
("Failed to check if the user is an administrator.", "Kunne ikke kontrollere, om brugeren er administrator."),
("Supported only in the installed version.", "Understøttes kun i den installerede version."),
("elevation_username_tip", "Indtast brugernavn eller domæne\\brugernavn"),
("Preparing for installation ...", "Forbereder installation ..."),
("Show my cursor", "Vis min markør"),
("Scale custom", "Tilpasset skalering"),
("Custom scale slider", "Skyder til tilpasset skalering"),
("Decrease", "Formindsk"),
("Increase", "Forøg"),
("Show virtual mouse", "Vis virtuel mus"),
("Virtual mouse size", "Størrelse på virtuel mus"),
("Small", "Lille"),
("Large", "Stor"),
("Show virtual joystick", "Vis virtuel joystick"),
("Edit note", "Redigér note"),
("Alias", "Alias"),
("ScrollEdge", "ScrollEdge"),
("Allow insecure TLS fallback", "Tillad usikker TLS-fallback"),
("allow-insecure-tls-fallback-tip", "Som standard verificerer RustDesk servercertifikatet for protokoller, der bruger TLS.\nNår denne indstilling er aktiveret, vil RustDesk springe verificeringstrinnet over og fortsætte, hvis verificeringen mislykkes."),
("Disable UDP", "Deaktivér UDP"),
("disable-udp-tip", "Bestemmer, om der kun skal bruges TCP.\nNår denne indstilling er aktiveret, vil RustDesk ikke længere bruge UDP 21116; i stedet bruges TCP 21116."),
("server-oss-not-support-tip", "BEMÆRK: RustDesk server OSS indeholder ikke denne funktion."),
("input note here", "indtast note her"),
("note-at-conn-end-tip", "Spørg om note ved afslutningen af forbindelsen"),
("Show terminal extra keys", "Vis ekstra terminaltaster"),
("Relative mouse mode", "Relativ musetilstand"),
("rel-mouse-not-supported-peer-tip", "Relativ musetilstand understøttes ikke af den tilsluttede modpart."),
("rel-mouse-not-ready-tip", "Relativ musetilstand er ikke klar endnu. Prøv venligst igen."),
("rel-mouse-lock-failed-tip", "Kunne ikke låse markøren. Relativ musetilstand er blevet deaktiveret."),
("rel-mouse-exit-{}-tip", "Tryk på {} for at afslutte."),
("rel-mouse-permission-lost-tip", "Tastaturtilladelsen blev tilbagekaldt. Relativ musetilstand er blevet deaktiveret."),
("Changelog", "Ændringslog"),
("keep-awake-during-outgoing-sessions-label", "Hold skærmen tændt under udgående sessioner"),
("keep-awake-during-incoming-sessions-label", "Hold skærmen tændt under indgående sessioner"),
("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", ""),
("Continue with {}", "Fortsæt med {}"),
("Display Name", "Visningsnavn"),
("password-hidden-tip", "Permanent adgangskode er indstillet (skjult)."),
("preset-password-in-use-tip", "Forudindstillet adgangskode er i øjeblikket i brug."),
("Enable privacy mode", "Aktivér privatlivstilstand"),
("allow-remote-toolbar-docking-any-edge", "Tillad fastgørelse af fjernværktøjslinjen til enhver vindueskant"),
("API Token", "API-token"),
("Deploy", "Udrul"),
("Custom ID (optional)", "Tilpasset ID (valgfrit)"),
("server_requires_deployment_tip", "Serveren kræver, at denne enhed udrulles eksplicit. Udrul nu?"),
("The server does not require explicit deployment.", "Serveren kræver ikke eksplicit udrulning."),
("Unknown response.", "Ukendt svar."),
("wayland-keyboard-input-disabled-tip", "Tillad tastaturinput?"),
("wayland-keyboard-input-consent-tip", "Det, du skriver på denne fjerncomputer (inklusive adgangskoder), kan blive læst af andre apps på den."),
("wayland-keyboard-input-applies-to-tip", "Dette valg gælder for:"),
("wayland-soft-keyboard-input-label", "Softwaretastaturinput"),
("wayland-keyboard-input-reset-choice-tip", "Nulstil valg for tastaturinput"),
("remember-wayland-keyboard-choice-tip", "Spørg ikke igen for denne fjerncomputer"),
("Why this happens", "Hvorfor dette sker"),
("Display Name", ""),
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
].iter().cloned().collect();
}

View File

@@ -743,20 +743,5 @@ 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", "Datenschutzmodus aktivieren"),
("allow-remote-toolbar-docking-any-edge", "Andocken der Remote-Symbolleiste an jeden Fensterrand zulassen"),
("API Token", "API-Token"),
("Deploy", "Bereitstellen"),
("Custom ID (optional)", "Benutzerdefinierte ID (optional)"),
("server_requires_deployment_tip", "Der Server erfordert, dass dieses Gerät explizit bereitgestellt wird. Jetzt bereitstellen?"),
("The server does not require explicit deployment.", "Der Server erfordert keine explizite Bereitstellung."),
("Unknown response.", "Unbekannte Antwort."),
("wayland-keyboard-input-disabled-tip", "Tastatureingabe zulassen?"),
("wayland-keyboard-input-consent-tip", "Was Sie auf diesem entfernten Computer eingeben (einschließlich Passwörter), könnte von anderen Apps darauf gelesen werden."),
("wayland-keyboard-input-applies-to-tip", "Diese Auswahl gilt für:"),
("wayland-soft-keyboard-input-label", "Bildschirmtastatureingabe"),
("wayland-keyboard-input-reset-choice-tip", "Auswahl der Tastatureingabe zurücksetzen"),
("remember-wayland-keyboard-choice-tip", "Für diesen entfernten Computer nicht erneut fragen"),
("Why this happens", "Warum dies passiert"),
].iter().cloned().collect();
}

View File

@@ -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", "Η καταγραφή οθόνης μέσω Wayland απέτυχε. Το XDG Desktop Portal ενδέχεται να έχει καταρρεύσει ή να μην είναι διαθέσιμο. Δοκιμάστε να το επανεκκινήσετε με `systemctl --user restart xdg-desktop-portal`."),
("xdp-portal-unavailable", ""),
("JumpLink", "Σύνδεσμος μετάβασης"),
("Please Select the screen to be shared(Operate on the peer side).", "Επιλέξτε την οθόνη που θέλετε να μοιραστείτε (Λειτουργία στην πλευρά του απομακρυσμένου σταθμού)."),
("Show RustDesk", "Εμφάνιση του RustDesk"),
@@ -741,22 +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", "Ο προκαθορισμένος κωδικός πρόσβασης χρησιμοποιείται αυτήν τη στιγμή."),
("Enable privacy mode", "Ενεργοποίηση λειτουργίας απορρήτου"),
("allow-remote-toolbar-docking-any-edge", "Να επιτρέπεται η προσάρτηση της απομακρυσμένης γραμμής εργαλείων σε οποιαδήποτε άκρη του παραθύρου"),
("API Token", "Διακριτικό API"),
("Deploy", "Ανάπτυξη"),
("Custom ID (optional)", "Προσαρμοσμένο ID (προαιρετικό)"),
("server_requires_deployment_tip", "Ο διακομιστής απαιτεί να αναπτυχθεί ρητά αυτή η συσκευή. Ανάπτυξη τώρα;"),
("The server does not require explicit deployment.", "Ο διακομιστής δεν απαιτεί ρητή ανάπτυξη."),
("Unknown response.", "Άγνωστη απάντηση."),
("wayland-keyboard-input-disabled-tip", "Να επιτρέπεται η εισαγωγή από το πληκτρολόγιο;"),
("wayland-keyboard-input-consent-tip", "Ό,τι πληκτρολογείτε σε αυτόν τον απομακρυσμένο υπολογιστή (συμπεριλαμβανομένων των κωδικών πρόσβασης) θα μπορούσε να διαβαστεί από άλλες εφαρμογές σε αυτόν."),
("wayland-keyboard-input-applies-to-tip", "Αυτή η επιλογή ισχύει για:"),
("wayland-soft-keyboard-input-label", "Εισαγωγή από εικονικό πληκτρολόγιο"),
("wayland-keyboard-input-reset-choice-tip", "Επαναφορά επιλογής εισαγωγής από πληκτρολόγιο"),
("remember-wayland-keyboard-choice-tip", "Να μην ερωτηθώ ξανά για αυτόν τον απομακρυσμένο υπολογιστή"),
("Why this happens", "Γιατί συμβαίνει αυτό"),
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
].iter().cloned().collect();
}

View File

@@ -218,6 +218,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("ab_web_console_tip", "More on web console"),
("allow-only-conn-window-open-tip", "Only allow connection if RustDesk window is open"),
("no_need_privacy_mode_no_physical_displays_tip", "No physical displays, no need to use the privacy mode."),
("Follow remote cursor", ""),
("Follow remote window focus", ""),
("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."),
@@ -229,6 +231,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("enable-bot-desc", "1. Open a chat with @BotFather.\n2. Send the command \"/newbot\". You will receive a token after completing this step.\n3. Start a chat with your newly created bot. Send a message beginning with a forward slash (\"/\") like \"/hello\" to activate it.\n"),
("cancel-2fa-confirm-tip", "Are you sure you want to cancel 2FA?"),
("cancel-bot-confirm-tip", "Are you sure you want to cancel Telegram bot?"),
("About RustDesk", ""),
("network_error_tip", "Please check your network connection, then click retry."),
("enable-trusted-devices-tip", "Skip 2FA verification on trusted devices"),
("one-way-file-transfer-tip", "One-way file transfer is enabled on the controlled side."),
@@ -271,13 +274,47 @@ 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"),
("server_requires_deployment_tip", "The server requires this device to be deployed explicitly. Deploy now?"),
("wayland-keyboard-input-disabled-tip", "Allow keyboard input?"),
("wayland-keyboard-input-consent-tip", "What you type on this remote computer (including passwords) could be read by other apps on it."),
("wayland-keyboard-input-applies-to-tip", "This choice applies to:"),
("wayland-soft-keyboard-input-label", "Soft keyboard input"),
("wayland-keyboard-input-reset-choice-tip", "Reset keyboard input choice"),
("remember-wayland-keyboard-choice-tip", "Don't ask again for this remote computer"),
("Keyboard Shortcuts", ""),
("Configure shortcuts...", ""),
("Enable keyboard shortcuts in remote session", ""),
("shortcut-page-description", "When enabled, listed key combinations trigger session actions locally instead of being sent to the remote. All bindings must include Ctrl+Alt+Shift (Cmd+Option+Shift on macOS) to avoid conflicts with normal typing."),
("Reset to defaults", ""),
("shortcut-reset-confirm-tip", "This will replace all current bindings with the default set. Continue?"),
("Session Control", ""),
("Display", ""),
("Other", ""),
("Toggle Fullscreen", ""),
("Switch to next display", ""),
("Switch to previous display", ""),
("View Mode 1:1", ""),
("View Mode Shrink", ""),
("View Mode Stretch", ""),
("Take Screenshot", ""),
("Toggle Audio", ""),
("Toggle Privacy Mode", ""),
("Toggle Recording", ""),
("Toggle Block User Input", ""),
("Switch Tab 1", ""),
("Switch Tab 2", ""),
("Switch Tab 3", ""),
("Switch Tab 4", ""),
("Switch Tab 5", ""),
("Switch Tab 6", ""),
("Switch Tab 7", ""),
("Switch Tab 8", ""),
("Switch Tab 9", ""),
("Edit", ""),
("Save", ""),
("Set Shortcut", ""),
("shortcut-recording-instruction", "Press the key combination you want to use."),
("shortcut-recording-press-keys-tip", "Press a key combination..."),
("shortcut-must-include-prefix", "Must include"),
("shortcut-already-bound-to", "Already bound to"),
("Replace", ""),
("Valid", ""),
("shortcut-mobile-physical-keyboard-tip", "Recording requires a physical keyboard. Soft keyboards are not supported."),
("Clear", ""),
("On", ""),
("Off", ""),
].iter().cloned().collect();
}

View File

@@ -125,7 +125,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Good image quality", "Bona bilda kvalito"),
("Balanced", "Normala bilda kvalito"),
("Optimize reaction time", "Optimigi reakcia tempo"),
("Custom", "Propra"),
("Custom", ""),
("Show remote cursor", "Montri foran kursoron"),
("Show quality monitor", "Montri kvalito monitoron"),
("Disable clipboard", "Malebligi poŝon"),
@@ -228,7 +228,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Username missed", "Uzantnomo forgesita"),
("Password missed", "Pasvorto forgesita"),
("Wrong credentials", "Identigilo aŭ pasvorto erara"),
("The verification code is incorrect or has expired", "La konfirmkodo estas malĝusta aŭ eksvalidiĝis"),
("The verification code is incorrect or has expired", ""),
("Edit Tag", "Redakti etikedo"),
("Forget Password", "Forgesi pasvorton"),
("Favorites", "Favorataj"),
@@ -305,7 +305,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Start on boot", "Komencu ĉe ekfunkciigo"),
("Start the screen sharing service on boot, requires special permissions", "Komencu la servon de kundivido de ekrano ĉe lanĉo, postulas specialajn permesojn"),
("Connection not allowed", "Konekto ne rajtas"),
("Legacy mode", "Malnova reĝimo"),
("Legacy mode", ""),
("Map mode", "Mapa modo"),
("Translate mode", "Traduki modo"),
("Use permanent password", "Uzu permanenta pasvorto"),
@@ -334,429 +334,414 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Insecure Connection", "Nesekura Konekto"),
("Scale original", "Skalo originalo"),
("Scale adaptive", "Skalo adapta"),
("General", "Ĝenerala"),
("Security", "Sekureco"),
("Theme", "Etoso"),
("Dark Theme", "Malhela etoso"),
("Light Theme", "Hela etoso"),
("Dark", "Malhela"),
("Light", "Hela"),
("Follow System", "Sekvi la sistemon"),
("Enable hardware codec", "Ebligi aparataran kodekon"),
("Unlock Security Settings", "Malŝlosi sekurecajn agordojn"),
("Enable audio", "Ebligi sonon"),
("Unlock Network Settings", "Malŝlosi retajn agordojn"),
("Server", "Servilo"),
("Direct IP Access", "Rekta IP-aliro"),
("Proxy", "Prokurilo"),
("Apply", "Apliki"),
("Disconnect all devices?", "Ĉu malkonekti ĉiujn aparatojn?"),
("Clear", "Vakigi"),
("Audio Input Device", "Aŭdia eniga aparato"),
("Use IP Whitelisting", "Uzi liston de IP akceptataj"),
("Network", "Reto"),
("Pin Toolbar", "Alpingli ilobreton"),
("Unpin Toolbar", "Malalpingli ilobreton"),
("Recording", "Registrado"),
("Directory", "Dosierujo"),
("Automatically record incoming sessions", "Aŭtomate registri envenajn sesiojn"),
("Automatically record outgoing sessions", "Aŭtomate registri elirajn sesiojn"),
("Change", "Ŝanĝi"),
("Start session recording", "Komenci sesian registradon"),
("Stop session recording", "Halti sesian registradon"),
("Enable recording session", "Ebligi sesian registradon"),
("Enable LAN discovery", "Ebligi malkovron en LAN"),
("Deny LAN discovery", "Malpermesi malkovron en LAN"),
("Write a message", "Skribi mesaĝon"),
("Prompt", "Demandi"),
("Please wait for confirmation of UAC...", "Bonvolu atendi la konfirmon de UAC..."),
("elevated_foreground_window_tip", "La aktuala fenestro de la fora labortablo bezonas pli altajn privilegiojn por funkcii, do provizore ne eblas uzi la muson kaj klavaron. Vi povas peti la foran uzanton minimumigi la aktualan fenestron, aŭ alklaki la altigan butonon en la fenestro de konekta administrado. Por eviti tiun problemon, oni rekomendas instali la programaron sur la fora aparato."),
("Disconnected", "Malkonektita"),
("Other", "Alia"),
("Confirm before closing multiple tabs", "Konfirmi antaŭ fermi plurajn langetojn"),
("Keyboard Settings", "Klavaraj agordoj"),
("Full Access", "Plena aliro"),
("Screen Share", "Kundivido de ekrano"),
("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", ""),
("Proxy", ""),
("Apply", ""),
("Disconnect all devices?", ""),
("Clear", ""),
("Audio Input Device", ""),
("Use IP Whitelisting", ""),
("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", ""),
("Deny LAN discovery", ""),
("Write a message", ""),
("Prompt", ""),
("Please wait for confirmation of UAC...", ""),
("elevated_foreground_window_tip", ""),
("Disconnected", ""),
("Other", ""),
("Confirm before closing multiple tabs", ""),
("Keyboard Settings", ""),
("Full Access", ""),
("Screen Share", ""),
("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", "La ekrankapto de Wayland malsukcesis. La XDG Desktop Portal eble kraŝis aŭ estas nedisponebla. Provu restarti ĝin per `systemctl --user restart xdg-desktop-portal`."),
("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", "Montri RustDesk"),
("This PC", "Ĉi tiu komputilo"),
("or", ""),
("Elevate", "Altigi"),
("Zoom cursor", "Zomi kursoron"),
("Accept sessions via password", "Akcepti sesiojn per pasvorto"),
("Accept sessions via click", "Akcepti sesiojn per alklako"),
("Accept sessions via both", "Akcepti sesiojn per ambaŭ"),
("Please wait for the remote side to accept your session request...", "Bonvolu atendi ke la fora flanko akceptu vian sesian peton..."),
("One-time Password", "Unufoja pasvorto"),
("Use one-time password", "Uzi unufojan pasvorton"),
("One-time password length", "Longeco de la unufoja pasvorto"),
("Request access to your device", "Peti aliron al via aparato"),
("Hide connection management window", "Kaŝi la fenestron de konekta administrado"),
("hide_cm_tip", "Permesi kaŝadon nur se oni akceptas sesiojn per pasvorto kaj uzas permanentan pasvorton"),
("wayland_experiment_tip", "La subteno de Wayland estas en eksperimenta stadio, bonvolu uzi X11 se vi bezonas senĉeestan aliron."),
("Right click to select tabs", "Dekstre alklaku por elekti langetojn"),
("Skipped", "Preterlasita"),
("Add to address book", "Aldoni al adresaro"),
("Group", "Grupo"),
("Search", "Serĉi"),
("Closed manually by web console", "Fermita permane de la reta konzolo"),
("Local keyboard type", "Tipo de loka klavaro"),
("Select local keyboard type", "Elekti tipon de loka klavaro"),
("software_render_tip", "Se vi uzas Nvidia-grafikkarton sub Linukso kaj la fora fenestro fermiĝas tuj post konektado, ŝanĝo al la malfermkoda Nouveau-pelilo kaj elekto de programara bildigo eble helpos. Programara restarto estas necesa."),
("Always use software rendering", "Ĉiam uzi programaran bildigon"),
("config_input", "Por kontroli la foran labortablon per klavaro, vi devas doni al RustDesk permeson \"Eniga Monitorado\"."),
("config_microphone", "Por paroli fore, vi devas doni al RustDesk permeson \"Registri Sonon\"."),
("request_elevation_tip", "Vi ankaŭ povas peti altigon se iu ĉeestas ĉe la fora flanko."),
("Wait", "Atendi"),
("Elevation Error", "Eraro de altigo"),
("Ask the remote user for authentication", "Peti aŭtentikigon de la fora uzanto"),
("Choose this if the remote account is administrator", "Elektu ĉi tion se la fora konto estas administranto"),
("Transmit the username and password of administrator", "Transsendi la uzantnomon kaj pasvorton de administranto"),
("still_click_uac_tip", "Ankoraŭ postulas ke la fora uzanto alklaku Bone en la UAC-fenestro de la funkcianta RustDesk."),
("Request Elevation", "Peti altigon"),
("wait_accept_uac_tip", "Bonvolu atendi ke la fora uzanto akceptu la UAC-dialogon."),
("Elevate successfully", "Altigo sukcesa"),
("uppercase", "majusklo"),
("lowercase", "minusklo"),
("digit", "cifero"),
("special character", "speciala signo"),
("length>=8", "longeco>=8"),
("Weak", "Malforta"),
("Medium", "Meza"),
("Strong", "Forta"),
("Switch Sides", "Interŝanĝi flankojn"),
("Please confirm if you want to share your desktop?", "Bonvolu konfirmi ĉu vi volas kundividi vian labortablon?"),
("Display", "Ekrano"),
("Default View Style", "Implicita rigarda stilo"),
("Default Scroll Style", "Implicita ruluma stilo"),
("Default Image Quality", "Implicita bilda kvalito"),
("Default Codec", "Implicita kodeko"),
("Bitrate", "Bitrapido"),
("FPS", "FPS"),
("Auto", "Aŭtomata"),
("Other Default Options", "Aliaj implicitaj opcioj"),
("Voice call", "Voĉvoko"),
("Text chat", "Teksta babilo"),
("Stop voice call", "Halti voĉvokon"),
("relay_hint_tip", "Eble ne eblas konekti rekte; vi povas provi konekti per relajso. Krome, se vi volas uzi relajson dum via unua provo, vi povas aldoni la sufikson \"/r\" al la identigilo aŭ elekti la opcion \"Ĉiam konekti per relajso\" en la karto de lastaj sesioj, se ĝi ekzistas."),
("Reconnect", "Rekonekti"),
("Codec", "Kodeko"),
("Resolution", "Distingivo"),
("No transfers in progress", "Neniu transigo en progreso"),
("Set one-time password length", "Agordi longecon de la unufoja pasvorto"),
("RDP Settings", "RDP-agordoj"),
("Sort by", "Ordigi laŭ"),
("New Connection", "Nova konekto"),
("Restore", "Restarigi"),
("Minimize", "Minimumigi"),
("Maximize", "Maksimumigi"),
("Your Device", "Via aparato"),
("empty_recent_tip", "Aĥ, neniuj lastaj sesioj!\nEstas tempo plani novan."),
("empty_favorite_tip", "Ankoraŭ neniuj favorataj samuloj?\nNi trovu iun por konekti kaj aldonu ĝin al viaj favorataj!"),
("empty_lan_tip", "Ho ve, ŝajnas ke ni ankoraŭ ne malkovris iujn samulojn."),
("empty_address_book_tip", "Ho ve, ŝajnas ke aktuale neniuj samuloj estas listigitaj en via adresaro."),
("Empty Username", "Malplena uzantnomo"),
("Empty Password", "Malplena pasvorto"),
("Me", "Mi"),
("identical_file_tip", "Ĉi tiu dosiero estas identa kun tiu de la samulo."),
("show_monitors_tip", "Montri monitorojn en la ilobreto"),
("View Mode", "Rigarda reĝimo"),
("login_linux_tip", "Vi devas ensaluti al la fora Linuksa konto por ebligi X-labortablan sesion"),
("verify_rustdesk_password_tip", "Kontroli RustDesk-pasvorton"),
("remember_account_tip", "Memori ĉi tiun konton"),
("os_account_desk_tip", "Ĉi tiu konto estas uzata por ensaluti al la fora operaciumo kaj ebligi la labortablan sesion en senekrana reĝimo"),
("OS Account", "Konto de operaciumo"),
("another_user_login_title_tip", "Alia uzanto jam ensalutis"),
("another_user_login_text_tip", "Malkonekti"),
("xorg_not_found_title_tip", "Xorg ne trovita"),
("xorg_not_found_text_tip", "Bonvolu instali Xorg"),
("no_desktop_title_tip", "Neniu labortabla medio disponeblas"),
("no_desktop_text_tip", "Bonvolu instali GNOME-labortablon"),
("No need to elevate", "Ne necesas altigi"),
("System Sound", "Sistema sono"),
("Default", "Implicita"),
("New RDP", "Nova RDP"),
("Fingerprint", "Fingrospuro"),
("Copy Fingerprint", "Kopii fingrospuron"),
("no fingerprints", "Neniuj fingrospuroj"),
("Select a peer", "Elekti samulon"),
("Select peers", "Elekti samulojn"),
("Plugins", "Kromprogramoj"),
("Uninstall", "Malinstali"),
("Update", "Ĝisdatigi"),
("Enable", "Ebligi"),
("Disable", "Malebligi"),
("Options", "Opcioj"),
("resolution_original_tip", "Originala distingivo"),
("resolution_fit_local_tip", "Adapti al loka distingivo"),
("resolution_custom_tip", "Propra distingivo"),
("Collapse toolbar", "Faldi ilobreton"),
("Accept and Elevate", "Akcepti kaj altigi"),
("accept_and_elevate_btn_tooltip", "Akcepti la konekton kaj altigi UAC-privilegiojn."),
("clipboard_wait_response_timeout_tip", "Tempolimo dum atendado de kopia respondo."),
("Incoming connection", "Enveninta konekto"),
("Outgoing connection", "Eliranta konekto"),
("Exit", "Eliri"),
("Open", "Malfermi"),
("logout_tip", "Ĉu vi certas ke vi volas elsaluti?"),
("Service", "Servo"),
("Start", "Komenci"),
("Stop", "Halti"),
("exceed_max_devices", "Vi atingis la maksimuman nombron da administrataj aparatoj."),
("Sync with recent sessions", "Sinkronigi kun lastaj sesioj"),
("Sort tags", "Ordigi etikedojn"),
("Open connection in new tab", "Malfermi konekton en nova langeto"),
("Move tab to new window", "Movi langeton al nova fenestro"),
("Can not be empty", "Ne povas esti malplena"),
("Already exists", "Jam ekzistas"),
("Change Password", "Ŝanĝi pasvorton"),
("Refresh Password", "Refreŝigi pasvorton"),
("ID", "Identigilo"),
("Grid View", "Krada rigardo"),
("List View", "Lista rigardo"),
("Select", "Elekti"),
("Toggle Tags", "Baskuligi etikedojn"),
("pull_ab_failed_tip", "Malsukcesis refreŝigi la adresaron"),
("push_ab_failed_tip", "Malsukcesis sinkronigi la adresaron al la servilo"),
("synced_peer_readded_tip", "La aparatoj kiuj ĉeestis en la lastaj sesioj estos sinkronigitaj reen al la adresaro."),
("Change Color", "Ŝanĝi koloron"),
("Primary Color", "Ĉefa koloro"),
("HSV Color", "HSV-koloro"),
("Installation Successful!", "Instalado sukcesa!"),
("Installation failed!", "Instalado malsukcesis!"),
("Reverse mouse wheel", "Inversigi musan radon"),
("{} sessions", "{} sesioj"),
("scam_title", "Vi Eble Estas TRUMPATA!"),
("scam_text1", "Se vi telefonas kun iu kiun vi NE konas KAJ FIDAS, kiu petis vin uzi RustDesk kaj komenci la servon, ne daŭrigu kaj tuj malŝaltu la telefonon."),
("scam_text2", "Ili verŝajne estas trompisto kiu provas ŝteli vian monon aŭ aliajn privatajn informojn."),
("Don't show again", "Ne montri denove"),
("I Agree", "Mi konsentas"),
("Decline", "Malakcepti"),
("Timeout in minutes", "Tempolimo en minutoj"),
("auto_disconnect_option_tip", "Aŭtomate fermi envenajn sesiojn ĉe uzanta neaktiveco"),
("Connection failed due to inactivity", "Aŭtomate malkonektita pro neaktiveco"),
("Check for software update on startup", "Kontroli pri programara ĝisdatigo ĉe ekfunkciigo"),
("upgrade_rustdesk_server_pro_to_{}_tip", "Bonvolu ĝisdatigi RustDesk Server Pro al versio {} aŭ pli nova!"),
("pull_group_failed_tip", "Malsukcesis refreŝigi la grupon"),
("Filter by intersection", "Filtri laŭ intersekco"),
("Remove wallpaper during incoming sessions", "Forigi la ekranfonon dum envenaj sesioj"),
("Test", "Testi"),
("display_is_plugged_out_msg", "La ekrano estas malkonektita, ŝanĝu al la unua ekrano."),
("No displays", "Neniuj ekranoj"),
("Open in new window", "Malfermi en nova fenestro"),
("Show displays as individual windows", "Montri ekranojn kiel apartajn fenestrojn"),
("Use all my displays for the remote session", "Uzi ĉiujn miajn ekranojn por la fora sesio"),
("selinux_tip", "SELinux estas ebligita sur via aparato, kio povas malhelpi RustDesk funkcii ĝuste kiel kontrolata flanko."),
("Change view", "Ŝanĝi rigardon"),
("Big tiles", "Grandaj kaheloj"),
("Small tiles", "Malgrandaj kaheloj"),
("List", "Listo"),
("Virtual display", "Virtuala ekrano"),
("Plug out all", "Malkonekti ĉiujn"),
("True color (4:4:4)", "Vera koloro (4:4:4)"),
("Enable blocking user input", "Ebligi blokadon de uzanta enigo"),
("id_input_tip", "Vi povas enigi identigilon, rektan IP, aŭ domajnon kun pordo (<domain>:<port>).\nSe vi volas aliri aparaton sur alia servilo, bonvolu aldoni la servilan adreson (<id>@<server_address>?key=<key_value>), ekzemple,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSe vi volas aliri aparaton sur publika servilo, bonvolu enigi \"<id>@public\", la ŝlosilo ne estas necesa por publika servilo.\n\nSe vi volas devigi la uzon de relajsa konekto ĉe la unua konekto, aldonu \"/r\" ĉe la fino de la identigilo, ekzemple, \"9123456234/r\"."),
("privacy_mode_impl_mag_tip", "Reĝimo 1"),
("privacy_mode_impl_virtual_display_tip", "Reĝimo 2"),
("Enter privacy mode", "Eniri privatan reĝimon"),
("Exit privacy mode", "Eliri privatan reĝimon"),
("idd_not_support_under_win10_2004_tip", "Nerekta ekrana pelilo ne estas subtenata. Windows 10, versio 2004 aŭ pli nova estas necesa."),
("input_source_1_tip", "Eniga fonto 1"),
("input_source_2_tip", "Eniga fonto 2"),
("Swap control-command key", "Interŝanĝi klavojn control kaj command"),
("swap-left-right-mouse", "Interŝanĝi maldekstran kaj dekstran musajn butonojn"),
("2FA code", "2FA-kodo"),
("More", "Pli"),
("enable-2fa-title", "Ebligi dufaktoran aŭtentikigon"),
("enable-2fa-desc", "Bonvolu agordi vian aŭtentikilon nun. Vi povas uzi aŭtentikilan aplikaĵon kiel Authy, Microsoft aŭ Google Authenticator sur via telefono aŭ komputilo.\n\nSkanu la QR-kodon per via aplikaĵo kaj enigu la kodon kiun via aplikaĵo montras por ebligi dufaktoran aŭtentikigon."),
("wrong-2fa-code", "Ne eblas kontroli la kodon. Kontrolu ke la kodo kaj la loka tempa agordo estas ĝustaj"),
("enter-2fa-title", "Dufaktora aŭtentikigo"),
("Email verification code must be 6 characters.", "La retpoŝta konfirmkodo devas esti 6 signoj."),
("2FA code must be 6 digits.", "La 2FA-kodo devas esti 6 ciferoj."),
("Multiple Windows sessions found", "Pluraj Windows-sesioj trovitaj"),
("Please select the session you want to connect to", "Bonvolu elekti la sesion al kiu vi volas konekti"),
("powered_by_me", "Funkciigita de RustDesk"),
("outgoing_only_desk_tip", "Ĉi tio estas personigita eldono.\nVi povas konekti al aliaj aparatoj, sed aliaj aparatoj ne povas konekti al via aparato."),
("preset_password_warning", "Ĉi tiu personigita eldono venas kun antaŭdifinita pasvorto. Iu ajn konanta ĉi tiun pasvorton povus akiri plenan kontrolon de via aparato. Se vi ne atendis ĉi tion, malinstalu la programaron tuj."),
("Security Alert", "Sekureca averto"),
("My address book", "Mia adresaro"),
("Personal", "Persona"),
("Owner", "Posedanto"),
("Set shared password", "Agordi kundividitan pasvorton"),
("Exist in", "Ekzistas en"),
("Read-only", "Nurlega"),
("Read/Write", "Lega/Skriba"),
("Full Control", "Plena kontrolo"),
("share_warning_tip", "La supraj kampoj estas kundividitaj kaj videblaj al aliaj."),
("Everyone", "Ĉiuj"),
("ab_web_console_tip", "Pli en la reta konzolo"),
("allow-only-conn-window-open-tip", "Permesi konekton nur se la fenestro de RustDesk estas malfermita"),
("no_need_privacy_mode_no_physical_displays_tip", "Neniuj fizikaj ekranoj, ne necesas uzi la privatan reĝimon."),
("Follow remote cursor", "Sekvi foran kursoron"),
("Follow remote window focus", "Sekvi fokuson de la fora fenestro"),
("default_proxy_tip", "La implicitaj protokolo kaj pordo estas Socks5 kaj 1080"),
("no_audio_input_device_tip", "Neniu aŭdia eniga aparato trovita."),
("Incoming", "Enveninta"),
("Outgoing", "Eliranta"),
("Clear Wayland screen selection", "Vakigi la ekranelekton de Wayland"),
("clear_Wayland_screen_selection_tip", "Post vakigo de la ekranelekto, vi povas reelekti la ekranon por kundividi."),
("confirm_clear_Wayland_screen_selection_tip", "Ĉu vi certas ke vi volas vakigi la ekranelekton de Wayland?"),
("android_new_voice_call_tip", "Nova voĉvoka peto estis ricevita. Se vi akceptas, la sono ŝanĝiĝos al voĉa komunikado."),
("texture_render_tip", "Uzi teksturan bildigon por glatigi la bildojn. Vi povus provi malebligi ĉi tiun opcion se vi renkontas bildigajn problemojn."),
("Use texture rendering", "Uzi teksturan bildigon"),
("Floating window", "Ŝveba fenestro"),
("floating_window_tip", "Ĝi helpas teni la fonan servon de RustDesk"),
("Keep screen on", "Teni la ekranon ŝaltita"),
("Never", "Neniam"),
("During controlled", "Dum kontrolata"),
("During service is on", "Dum la servo estas ŝaltita"),
("Capture screen using DirectX", "Kapti ekranon per DirectX"),
("Back", "Reen"),
("Apps", "Aplikaĵoj"),
("Volume up", "Plilaŭtigi"),
("Volume down", "Mallaŭtigi"),
("Power", "Ŝalto"),
("Telegram bot", "Telegram-roboto"),
("enable-bot-tip", "Se vi ebligas ĉi tiun funkcion, vi povas ricevi la 2FA-kodon de via roboto. Ĝi ankaŭ povas funkcii kiel konekta sciigo."),
("enable-bot-desc", "1. Malfermu babilon kun @BotFather.\n2. Sendu la komandon \"/newbot\". Vi ricevos ĵetonon post plenumi ĉi tiun paŝon.\n3. Komencu babilon kun via nove kreita roboto. Sendu mesaĝon komenciĝantan per oblikva streko (\"/\") kiel \"/hello\" por aktivigi ĝin.\n"),
("cancel-2fa-confirm-tip", "Ĉu vi certas ke vi volas nuligi 2FA?"),
("cancel-bot-confirm-tip", "Ĉu vi certas ke vi volas nuligi la Telegram-roboton?"),
("About RustDesk", "Pri RustDesk"),
("Send clipboard keystrokes", "Sendi poŝajn klavpremojn"),
("network_error_tip", "Bonvolu kontroli vian retkonekton, poste alklaku reprovi."),
("Unlock with PIN", "Malŝlosi per PIN"),
("Requires at least {} characters", "Bezonas almenaŭ {} signojn"),
("Wrong PIN", "Malĝusta PIN"),
("Set PIN", "Agordi PIN"),
("Enable trusted devices", "Ebligi fidatajn aparatojn"),
("Manage trusted devices", "Administri fidatajn aparatojn"),
("Platform", "Platformo"),
("Days remaining", "Restantaj tagoj"),
("enable-trusted-devices-tip", "Preterlasi 2FA-kontrolon sur fidataj aparatoj"),
("Parent directory", "Patra dosierujo"),
("Resume", "Daŭrigi"),
("Invalid file name", "Nevalida dosiernomo"),
("one-way-file-transfer-tip", "Unudirekta dosiertransigo estas ebligita ĉe la kontrolata flanko."),
("Authentication Required", "Aŭtentikigo necesa"),
("Authenticate", "Aŭtentikigi"),
("web_id_input_tip", "Vi povas enigi identigilon en la sama servilo, rekta IP-aliro ne estas subtenata en la reta kliento.\nSe vi volas aliri aparaton sur alia servilo, bonvolu aldoni la servilan adreson (<id>@<server_address>?key=<key_value>), ekzemple,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSe vi volas aliri aparaton sur publika servilo, bonvolu enigi \"<id>@public\", la ŝlosilo ne estas necesa por publika servilo."),
("Download", "Elŝuti"),
("Upload folder", "Alŝuti dosierujon"),
("Upload files", "Alŝuti dosierojn"),
("Clipboard is synchronized", "La poŝo estas sinkronigita"),
("Update client clipboard", "Ĝisdatigi la poŝon de la kliento"),
("Untagged", "Sen etikedo"),
("new-version-of-{}-tip", "Nova versio de {} disponeblas"),
("Accessible devices", "Alireblaj aparatoj"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Bonvolu ĝisdatigi la RustDesk-klienton al versio {} aŭ pli nova ĉe la fora flanko!"),
("d3d_render_tip", "Kiam D3D-bildigo estas ebligita, la ekrano de fora kontrolo povas esti nigra sur kelkaj maŝinoj."),
("Use D3D rendering", "Uzi D3D-bildigon"),
("Printer", "Presilo"),
("printer-os-requirement-tip", "La eliranta presa funkcio bezonas Windows 10 aŭ pli novan."),
("printer-requires-installed-{}-client-tip", "Por uzi foran presadon, {} devas esti instalita sur ĉi tiu aparato."),
("printer-{}-not-installed-tip", "La presilo {} ne estas instalita."),
("printer-{}-ready-tip", "La presilo {} estas instalita kaj preta por uzo."),
("Install {} Printer", "Instali presilon {}"),
("Outgoing Print Jobs", "Elirantaj presaj taskoj"),
("Incoming Print Jobs", "Envenantaj presaj taskoj"),
("Incoming Print Job", "Enveninta presa tasko"),
("use-the-default-printer-tip", "Uzi la implicitan presilon"),
("use-the-selected-printer-tip", "Uzi la elektitan presilon"),
("auto-print-tip", "Presi aŭtomate uzante la elektitan presilon."),
("print-incoming-job-confirm-tip", "Vi ricevis presan taskon de fore. Ĉu vi volas plenumi ĝin ĉe via flanko?"),
("remote-printing-disallowed-tile-tip", "Fora presado malpermesita"),
("remote-printing-disallowed-text-tip", "La permesaj agordoj de la kontrolata flanko malpermesas foran presadon."),
("save-settings-tip", "Konservi agordojn"),
("dont-show-again-tip", "Ne montri ĉi tion denove"),
("Take screenshot", "Fari ekrankopion"),
("Taking screenshot", "Faras ekrankopion"),
("screenshot-merged-screen-not-supported-tip", "Kunfandi ekrankopiojn de pluraj ekranoj aktuale ne estas subtenata. Bonvolu ŝanĝi al unu ekrano kaj reprovi."),
("screenshot-action-tip", "Bonvolu elekti kiel daŭrigi kun la ekrankopio."),
("Save as", "Konservi kiel"),
("Copy to clipboard", "Kopii al la poŝo"),
("Enable remote printer", "Ebligi foran presilon"),
("Downloading {}", "Elŝutas {}"),
("{} Update", "Ĝisdatigo de {}"),
("{}-to-update-tip", "{} fermiĝos nun kaj instalos la novan version."),
("download-new-version-failed-tip", "Elŝuto malsukcesis. Vi povas reprovi aŭ alklaki la butonon \"Elŝuti\" por elŝuti de la eldona paĝo kaj ĝisdatigi permane."),
("Auto update", "Aŭtomata ĝisdatigo"),
("update-failed-check-msi-tip", "Kontrolo de instala metodo malsukcesis. Bonvolu alklaki la butonon \"Elŝuti\" por elŝuti de la eldona paĝo kaj ĝisdatigi permane."),
("websocket_tip", "Kiam oni uzas WebSocket, nur relajsaj konektoj estas subtenataj."),
("Use WebSocket", "Uzi WebSocket"),
("Trackpad speed", "Rapideco de tuŝplato"),
("Default trackpad speed", "Implicita rapideco de tuŝplato"),
("Numeric one-time password", "Numera unufoja pasvorto"),
("Enable IPv6 P2P connection", "Ebligi IPv6 P2P-konekton"),
("Enable UDP hole punching", "Ebligi UDP-trapikadon"),
("Show 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", ""),
("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", ""),
("Request Elevation", ""),
("wait_accept_uac_tip", ""),
("Elevate successfully", ""),
("uppercase", ""),
("lowercase", ""),
("digit", ""),
("special character", ""),
("length>=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", ""),
("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", ""),
("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", ""),
("remember_account_tip", ""),
("os_account_desk_tip", ""),
("OS Account", ""),
("another_user_login_title_tip", ""),
("another_user_login_text_tip", ""),
("xorg_not_found_title_tip", ""),
("xorg_not_found_text_tip", ""),
("no_desktop_title_tip", ""),
("no_desktop_text_tip", ""),
("No need to elevate", ""),
("System Sound", ""),
("Default", ""),
("New 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", ""),
("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", ""),
("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", ""),
("View camera", "Rigardi kameron"),
("Enable camera", "Ebligi kameron"),
("No cameras", "Neniuj kameroj"),
("view_camera_unsupported_tip", "La fora aparato ne subtenas rigardadon de la kamero."),
("Terminal", "Terminalo"),
("Enable terminal", "Ebligi terminalon"),
("New tab", "Nova langeto"),
("Keep terminal sessions on disconnect", "Teni terminalajn sesiojn ĉe malkonekto"),
("Terminal (Run as administrator)", "Terminalo (Ruli kiel administranto)"),
("terminal-admin-login-tip", "Bonvolu enigi la administran uzantnomon kaj pasvorton de la kontrolata flanko."),
("Failed to get user token.", "Malsukcesis akiri uzantan ĵetonon."),
("Incorrect username or password.", "Malĝusta uzantnomo aŭ pasvorto."),
("The user is not an administrator.", "La uzanto ne estas administranto."),
("Failed to check if the user is an administrator.", "Malsukcesis kontroli ĉu la uzanto estas administranto."),
("Supported only in the installed version.", "Subtenata nur en la instalita versio."),
("elevation_username_tip", "Enigu uzantnomon aŭ domajno\\uzantnomo"),
("Preparing for installation ...", "Preparas por instalado ..."),
("Show my cursor", "Montri mian kursoron"),
("Scale custom", "Propra skalo"),
("Custom scale slider", "Ŝovilo de propra skalo"),
("Decrease", "Malpliigi"),
("Increase", "Pliigi"),
("Show virtual mouse", "Montri virtualan muson"),
("Virtual mouse size", "Grandeco de virtuala muso"),
("Small", "Malgranda"),
("Large", "Granda"),
("Show virtual joystick", "Montri virtualan stirstangon"),
("Edit note", "Redakti noton"),
("Alias", "Kromnomo"),
("ScrollEdge", "ScrollEdge"),
("Allow insecure TLS fallback", "Permesi nesekuran TLS-retrokadron"),
("allow-insecure-tls-fallback-tip", "Implicite, RustDesk kontrolas la servilan atestilon por protokoloj uzantaj TLS.\nKun ĉi tiu opcio ebligita, RustDesk retrokadros al preterlaso de la kontrola paŝo kaj daŭrigos en kazo de kontrola malsukceso."),
("Disable UDP", "Malebligi UDP"),
("disable-udp-tip", "Regas ĉu uzi nur TCP.\nKiam ĉi tiu opcio estas ebligita, RustDesk ne plu uzos UDP 21116, TCP 21116 estos uzata anstataŭe."),
("server-oss-not-support-tip", "NOTU: RustDesk-servilo OSS ne inkluzivas ĉi tiun funkcion."),
("input note here", "enigu noton ĉi tie"),
("note-at-conn-end-tip", "Peti noton ĉe la fino de la konekto"),
("Show terminal extra keys", "Montri kromajn klavojn de terminalo"),
("Relative mouse mode", "Relativa musa reĝimo"),
("rel-mouse-not-supported-peer-tip", "Relativa musa reĝimo ne estas subtenata de la konektita samulo."),
("rel-mouse-not-ready-tip", "Relativa musa reĝimo ankoraŭ ne estas preta. Bonvolu reprovi."),
("rel-mouse-lock-failed-tip", "Malsukcesis ŝlosi la kursoron. Relativa musa reĝimo estis malebligita."),
("rel-mouse-exit-{}-tip", "Premu {} por eliri."),
("rel-mouse-permission-lost-tip", "Klavara permeso estis revokita. Relativa musa reĝimo estis malebligita."),
("Changelog", "Ŝanĝoprotokolo"),
("keep-awake-during-outgoing-sessions-label", "Teni la ekranon veka dum elirantaj sesioj"),
("keep-awake-during-incoming-sessions-label", "Teni la ekranon veka dum envenantaj sesioj"),
("Continue with {}", "Daŭrigi per {}"),
("Display Name", "Montrata nomo"),
("password-hidden-tip", "Permanenta pasvorto estas agordita (kaŝita)."),
("preset-password-in-use-tip", "Antaŭdifinita pasvorto estas aktuale uzata."),
("Enable privacy mode", "Ebligi privatan reĝimon"),
("allow-remote-toolbar-docking-any-edge", "Permesi alfiksi la foran ilobreton al iu ajn rando de fenestro"),
("API Token", "API-ĵetono"),
("Deploy", "Disfaldi"),
("Custom ID (optional)", "Propra identigilo (laŭvola)"),
("server_requires_deployment_tip", "La servilo postulas ke ĉi tiu aparato estu eksplicite disfaldita. Ĉu disfaldi nun?"),
("The server does not require explicit deployment.", "La servilo ne postulas eksplicitan disfaldon."),
("Unknown response.", "Nekonata respondo."),
("wayland-keyboard-input-disabled-tip", "Ĉu permesi klavaran enigon?"),
("wayland-keyboard-input-consent-tip", "Tio kion vi tajpas sur ĉi tiu fora komputilo (inkluzive pasvortojn) povus esti legata de aliaj aplikaĵoj sur ĝi."),
("wayland-keyboard-input-applies-to-tip", "Ĉi tiu elekto validas por:"),
("wayland-soft-keyboard-input-label", "Programa klavara enigo"),
("wayland-keyboard-input-reset-choice-tip", "Restarigi la elekton de klavara enigo"),
("remember-wayland-keyboard-choice-tip", "Ne demandi denove por ĉi tiu fora komputilo"),
("Why this happens", "Kial ĉi tio okazas"),
("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", ""),
("Continue with {}", ""),
("Display Name", ""),
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
].iter().cloned().collect();
}

View File

@@ -67,14 +67,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Skip", "Omitir"),
("Close", "Cerrar"),
("Retry", "Reintentar"),
("OK", "Aceptar"),
("OK", ""),
("Password Required", "Se requiere contraseña"),
("Please enter your password", "Por favor, introduzca su contraseña"),
("Remember password", "Recordar contraseña"),
("Wrong Password", "Contraseña incorrecta"),
("Do you want to enter again?", "¿Quieres volver a entrar?"),
("Connection Error", "Error de conexión"),
("Error", "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"),
("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", "Conectar a través de relay"),
("Connect via 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", "El código de verificación es incorrecto o ha caducado"),
("The verification code is incorrect or has expired", ""),
("Edit Tag", "Editar tag"),
("Forget Password", "Olvidar contraseña"),
("Favorites", "Favoritos"),
@@ -282,8 +282,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("android_service_will_start_tip", "Habilitar la captura de pantalla iniciará automáticamente el servicio, lo que permitirá que otros dispositivos soliciten una conexión desde este dispositivo."),
("android_stop_service_tip", "Cerrar el servicio cerrará automáticamente todas las conexiones establecidas."),
("android_version_audio_tip", "La versión actual de Android no admite la captura de audio, actualice a Android 10 o posterior."),
("android_start_service_tip", "Toque [Iniciar servicio] o conceda el permiso [Captura de pantalla] para iniciar el servicio de pantalla compartida."),
("android_permission_may_not_change_tip", "Es posible que los permisos de las conexiones ya establecidas no cambien de inmediato hasta que se vuelva a conectar."),
("android_start_service_tip", ""),
("android_permission_may_not_change_tip", ""),
("Account", "Cuenta"),
("Overwrite", "Sobrescribir"),
("This file exists, skip or overwrite this file?", "Este archivo existe, ¿omitir o sobrescribir este archivo?"),
@@ -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", "Iniciar al arrancar"),
("Start the screen sharing service on boot, requires special permissions", "Iniciar el servicio de pantalla compartida al arrancar, requiere permisos especiales"),
("Start on boot", ""),
("Start the screen sharing service on boot, requires special permissions", ""),
("Connection not allowed", "Conexión no disponible"),
("Legacy mode", "Modo heredado"),
("Map mode", "Modo mapa"),
@@ -326,19 +326,19 @@ 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", "Mostrar herramientas"),
("Hide Toolbar", "Ocultar herramientas"),
("Show Toolbar", ""),
("Hide Toolbar", ""),
("Direct Connection", "Conexión directa"),
("Relay Connection", "Conexión Relay"),
("Secure Connection", "Conexión segura"),
("Insecure Connection", "Conexión insegura"),
("Scale original", "Escala original"),
("Scale adaptive", "Escala adaptativa"),
("General", "General"),
("General", ""),
("Security", "Seguridad"),
("Theme", "Tema"),
("Dark Theme", "Tema Oscuro"),
("Light Theme", "Tema claro"),
("Light Theme", ""),
("Dark", "Oscuro"),
("Light", "Claro"),
("Follow System", "Tema del sistema"),
@@ -348,19 +348,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Unlock Network Settings", "Desbloquear Ajustes de Red"),
("Server", "Servidor"),
("Direct IP Access", "Acceso IP Directo"),
("Proxy", "Proxy"),
("Proxy", ""),
("Apply", "Aplicar"),
("Disconnect all devices?", "¿Desconectar todos los dispositivos?"),
("Clear", "Borrar"),
("Audio Input Device", "Dispositivo de entrada de audio"),
("Use IP Whitelisting", "Usar lista de IPs admitidas"),
("Network", "Red"),
("Pin Toolbar", "Anclar herramientas"),
("Unpin Toolbar", "Desanclar herramientas"),
("Pin Toolbar", ""),
("Unpin Toolbar", ""),
("Recording", "Grabando"),
("Directory", "Directorio"),
("Automatically record incoming sessions", "Grabación automática de sesiones entrantes"),
("Automatically record outgoing sessions", "Grabación automática de sesiones salientes"),
("Automatically record outgoing sessions", ""),
("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", "Solicitud"),
("Prompt", ""),
("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"),
@@ -379,7 +379,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Screen Share", "Compartir pantalla"),
("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", "Error en la captura de pantalla de Wayland. Es posible que el XDG Desktop Portal se haya bloqueado o no esté disponible. Intente reiniciarlo con `systemctl --user restart xdg-desktop-portal`."),
("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"),
@@ -436,8 +436,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Default Image Quality", "Calidad de imagen predeterminada"),
("Default Codec", "Códec predeterminado"),
("Bitrate", "Tasa de bits"),
("FPS", "FPS"),
("Auto", "Automático"),
("FPS", ""),
("Auto", ""),
("Other Default Options", "Otras opciones predeterminadas"),
("Voice call", "Llamada de voz"),
("Text chat", "Chat de texto"),
@@ -515,7 +515,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Already exists", "Ya existe"),
("Change Password", "Cambiar contraseña"),
("Refresh Password", "Refrescar contraseña"),
("ID", "ID"),
("ID", ""),
("Grid View", "Vista Cuadrícula"),
("List View", "Vista Lista"),
("Select", "Seleccionar"),
@@ -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", "Aplicaciones"),
("Volume up", "Subir volumen"),
("Volume down", "Bajar volumen"),
("Apps", ""),
("Volume up", "Bajar volumen"),
("Volume down", "Subir 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", "Dispositivos accesibles"),
("Accessible devices", ""),
("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,14 +689,14 @@ 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", "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"),
("Numeric one-time password", ""),
("Enable IPv6 P2P connection", ""),
("Enable UDP hole punching", ""),
("View camera", "Ver cámara"),
("Enable camera", "Habilitar cámara"),
("No cameras", "No hay cámaras"),
("view_camera_unsupported_tip", "El dispositivo remoto no soporta la visualización de la cámara."),
("Terminal", "Terminal"),
("Terminal", ""),
("Enable terminal", "Habilitar terminal"),
("New tab", "Nueva pestaña"),
("Keep terminal sessions on disconnect", "Mantener sesiones de terminal al desconectar"),
@@ -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 ...", "Preparando instlación..."),
("Show my cursor", "Mostrar mi cursor"),
("Preparing for installation ...", ""),
("Show my cursor", ""),
("Scale custom", "Escala personalizada"),
("Custom scale slider", "Control deslizante de escala personalizada"),
("Decrease", "Disminuir"),
@@ -720,43 +720,28 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Large", "Grande"),
("Show virtual joystick", "Mostrar joystick virtual"),
("Edit note", "Editar nota"),
("Alias", "Alias"),
("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"),
("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", ""),
("Continue with {}", "Continuar con {}"),
("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"),
("allow-remote-toolbar-docking-any-edge", "Permitir acoplar la barra de herramientas remota a cualquier borde de la ventana"),
("API Token", "Token de API"),
("Deploy", "Implementar"),
("Custom ID (optional)", "ID personalizado (opcional)"),
("server_requires_deployment_tip", "El servidor requiere que este dispositivo se implemente de forma explícita. ¿Implementar ahora?"),
("The server does not require explicit deployment.", "El servidor no requiere una implementación explícita."),
("Unknown response.", "Respuesta desconocida."),
("wayland-keyboard-input-disabled-tip", "¿Permitir la entrada del teclado?"),
("wayland-keyboard-input-consent-tip", "Lo que escriba en este equipo remoto (incluidas las contraseñas) podría ser leído por otras aplicaciones del mismo."),
("wayland-keyboard-input-applies-to-tip", "Esta opción se aplica a:"),
("wayland-soft-keyboard-input-label", "Entrada del teclado virtual"),
("wayland-keyboard-input-reset-choice-tip", "Restablecer la opción de entrada del teclado"),
("remember-wayland-keyboard-choice-tip", "No volver a preguntar para este equipo remoto"),
("Why this happens", "Por qué ocurre esto"),
("Display Name", ""),
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
].iter().cloned().collect();
}

View File

@@ -379,7 +379,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Screen Share", "Ekraanijagamine"),
("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", "Wayland ekraani jäädvustamine ebaõnnestus. XDG Desktop Portal võis kokku joosta või pole saadaval. Proovi see taaskäivitada käsuga `systemctl --user restart xdg-desktop-portal`."),
("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"),
@@ -653,110 +653,95 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("new-version-of-{}-tip", "Saadaval on {} uus versioon"),
("Accessible devices", "Ligipääsetavad seadmed"),
("upgrade_remote_rustdesk_client_to_{}_tip", "Täiendage RustDeski klient kaugküljel versioonile {} või uuemale!"),
("d3d_render_tip", "Kui D3D renderdamine on lubatud, võib kaugjuhtimise ekraan mõnel masinal olla must."),
("Use D3D rendering", "Kasuta D3D renderdamist"),
("Printer", "Printer"),
("printer-os-requirement-tip", "Printeri väljuva töö funktsioon nõuab Windows 10 või uuemat."),
("printer-requires-installed-{}-client-tip", "Kaugprintimise kasutamiseks tuleb sellesse seadmesse installida {}."),
("printer-{}-not-installed-tip", "Printerit {} pole installitud."),
("printer-{}-ready-tip", "Printer {} on installitud ja kasutusvalmis."),
("Install {} Printer", "Installi printer {}"),
("Outgoing Print Jobs", "Väljuvad prinditööd"),
("Incoming Print Jobs", "Sissetulevad prinditööd"),
("Incoming Print Job", "Sissetulev prinditöö"),
("use-the-default-printer-tip", "Kasuta vaikeprinterit"),
("use-the-selected-printer-tip", "Kasuta valitud printerit"),
("auto-print-tip", "Prindi automaatselt valitud printeriga."),
("print-incoming-job-confirm-tip", "Sa said kaugarvutist prinditöö. Kas soovid selle oma poolel teostada?"),
("remote-printing-disallowed-tile-tip", "Kaugprintimine pole lubatud"),
("remote-printing-disallowed-text-tip", "Juhitava poole õiguste seaded keelavad kaugprintimise."),
("save-settings-tip", "Salvesta seaded"),
("dont-show-again-tip", "Ära kuva seda uuesti"),
("Take screenshot", "Tee kuvatõmmis"),
("Taking screenshot", "Kuvatõmmise tegemine"),
("screenshot-merged-screen-not-supported-tip", "Mitme kuva kuvatõmmiste ühendamine pole praegu toetatud. Palun lülitu ühele kuvale ja proovi uuesti."),
("screenshot-action-tip", "Palun vali, kuidas kuvatõmmisega jätkata."),
("Save as", "Salvesta kui"),
("Copy to clipboard", "Kopeeri lõikelauale"),
("Enable remote printer", "Luba kaugprinter"),
("Downloading {}", "Allalaadimine: {}"),
("{} Update", "{} uuendus"),
("{}-to-update-tip", "{} sulgub nüüd ja installib uue versiooni."),
("download-new-version-failed-tip", "Allalaadimine ebaõnnestus. Võid proovida uuesti või klõpsata nuppu \"Laadi alla\", et laadida fail alla väljalaskelehelt ja uuendada käsitsi."),
("Auto update", "Automaatne uuendamine"),
("update-failed-check-msi-tip", "Installimismeetodi kontroll ebaõnnestus. Palun klõpsa nuppu \"Laadi alla\", et laadida fail alla väljalaskelehelt ja uuendada käsitsi."),
("websocket_tip", "WebSocketi kasutamisel on toetatud ainult releeühendused."),
("Use WebSocket", "Kasuta WebSocketit"),
("Trackpad speed", "Puuteplaadi kiirus"),
("Default trackpad speed", "Vaikimisi puuteplaadi kiirus"),
("Numeric one-time password", "Numbriline ühekordne parool"),
("Enable IPv6 P2P connection", "Luba IPv6 P2P-ühendus"),
("Enable UDP hole punching", "Luba UDP-augustamine"),
("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", "Vaata kaamerat"),
("Enable camera", "Luba kaamera"),
("No cameras", "Kaameraid pole"),
("view_camera_unsupported_tip", "Kaugseade ei toeta kaamera vaatamist."),
("Terminal", "Terminal"),
("Enable terminal", "Luba terminal"),
("New tab", "Uus kaart"),
("Keep terminal sessions on disconnect", "Säilita terminaliseansid ühenduse katkemisel"),
("Terminal (Run as administrator)", "Terminal (käivita administraatorina)"),
("terminal-admin-login-tip", "Palun sisesta juhitava poole administraatori kasutajanimi ja parool."),
("Failed to get user token.", "Kasutaja loa hankimine ebaõnnestus."),
("Incorrect username or password.", "Vale kasutajanimi või parool."),
("The user is not an administrator.", "Kasutaja ei ole administraator."),
("Failed to check if the user is an administrator.", "Kasutaja administraatoriõiguste kontrollimine ebaõnnestus."),
("Supported only in the installed version.", "Toetatud ainult installitud versioonis."),
("elevation_username_tip", "Sisesta kasutajanimi või domeen\\kasutajanimi"),
("Preparing for installation ...", "Installimiseks valmistumine ..."),
("Show my cursor", "Näita minu kursorit"),
("Scale custom", "Kohandatud mastaap"),
("Custom scale slider", "Kohandatud mastaabi liugur"),
("Decrease", "Vähenda"),
("Increase", "Suurenda"),
("Show virtual mouse", "Näita virtuaalset hiirt"),
("Virtual mouse size", "Virtuaalse hiire suurus"),
("Small", "Väike"),
("Large", "Suur"),
("Show virtual joystick", "Näita virtuaalset juhtkangi"),
("Edit note", "Muuda märkust"),
("Alias", "Alias"),
("ScrollEdge", "ScrollEdge"),
("Allow insecure TLS fallback", "Luba ebaturvaline TLS-i tagavaravariant"),
("allow-insecure-tls-fallback-tip", "Vaikimisi kontrollib RustDesk TLS-i kasutavate protokollide puhul serveri sertifikaati.\nKui see valik on lubatud, jätab RustDesk kontrollimissammu vahele ja jätkab kontrolli ebaõnnestumise korral."),
("Disable UDP", "Keela UDP"),
("disable-udp-tip", "Määrab, kas kasutada ainult TCP-d.\nKui see valik on lubatud, ei kasuta RustDesk enam UDP 21116, vaid kasutatakse selle asemel TCP 21116."),
("server-oss-not-support-tip", "MÄRKUS: RustDeski serveri OSS ei sisalda seda funktsiooni."),
("input note here", "sisesta märkus siia"),
("note-at-conn-end-tip", "Küsi märkust ühenduse lõpus"),
("Show terminal extra keys", "Näita terminali lisaklahve"),
("Relative mouse mode", "Suhteline hiirerežiim"),
("rel-mouse-not-supported-peer-tip", "Ühendatud partner ei toeta suhtelist hiirerežiimi."),
("rel-mouse-not-ready-tip", "Suhteline hiirerežiim ei ole veel valmis. Palun proovi uuesti."),
("rel-mouse-lock-failed-tip", "Kursori lukustamine ebaõnnestus. Suhteline hiirerežiim on keelatud."),
("rel-mouse-exit-{}-tip", "Väljumiseks vajuta {}."),
("rel-mouse-permission-lost-tip", "Klaviatuuriõigus võeti tagasi. Suhteline hiirerežiim on keelatud."),
("Changelog", "Muudatuste logi"),
("keep-awake-during-outgoing-sessions-label", "Hoia ekraan ärkvel väljuvate seansside ajal"),
("keep-awake-during-incoming-sessions-label", "Hoia ekraan ärkvel sissetulevate seansside ajal"),
("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", ""),
("Continue with {}", "Jätka koos {}"),
("Display Name", "Kuvatav nimi"),
("password-hidden-tip", "Püsiparool on seatud (peidetud)."),
("preset-password-in-use-tip", "Praegu on kasutusel eelseadistatud parool."),
("Enable privacy mode", "Luba privaatsusrežiim"),
("allow-remote-toolbar-docking-any-edge", "Luba kaugjuhtimise tööriistariba dokkimine mis tahes akna servale"),
("API Token", "API luba"),
("Deploy", "Juuruta"),
("Custom ID (optional)", "Kohandatud ID (valikuline)"),
("server_requires_deployment_tip", "Server nõuab selle seadme selgesõnalist juurutamist. Kas juurutada kohe?"),
("The server does not require explicit deployment.", "Server ei nõua selgesõnalist juurutamist."),
("Unknown response.", "Tundmatu vastus."),
("wayland-keyboard-input-disabled-tip", "Kas lubada klaviatuurisisestus?"),
("wayland-keyboard-input-consent-tip", "Seda, mida sa sellesse kaugarvutisse sisestad (sealhulgas paroolid), võivad lugeda seal olevad teised rakendused."),
("wayland-keyboard-input-applies-to-tip", "See valik kehtib järgmisele:"),
("wayland-soft-keyboard-input-label", "Tarkvaralise klaviatuuri sisestus"),
("wayland-keyboard-input-reset-choice-tip", "Lähtesta klaviatuurisisestuse valik"),
("remember-wayland-keyboard-choice-tip", "Ära küsi selle kaugarvuti puhul uuesti"),
("Why this happens", "Miks see juhtub"),
("Display Name", ""),
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
].iter().cloned().collect();
}

View File

@@ -360,7 +360,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Recording", "Grabatzen"),
("Directory", "Direktorioa"),
("Automatically record incoming sessions", "Automatikoki grabatu sarrerako saioak"),
("Automatically record outgoing sessions", "Grabatu automatikoki irteerako saioak"),
("Automatically record outgoing sessions", ""),
("Change", "Aldatu"),
("Start session recording", "Hasi saioaren grabaketa"),
("Stop session recording", "Gelditu saioaren grabaketa"),
@@ -379,7 +379,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Screen Share", "Pantailaren partekatzea"),
("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", "Wayland-eko pantaila-kapturak huts egin du. Baliteke XDG Desktop Portal kraskatu izana edo erabilgarri ez egotea. Saiatu berrabiarazten `systemctl --user restart xdg-desktop-portal` erabiliz."),
("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"),
@@ -623,140 +623,125 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Telegram bot", "Telegrameko bot-a"),
("enable-bot-tip", "Ezaugarri hau gaitzen baduzu, zure bot-etik 2FA kodea jaso dezakezu. Konexio jakinarazpenetarako ere balio dezake."),
("enable-bot-desc", "1, Ireki txat bat @BotFather bot-arekin.\n2, Bidali \"/newbot\" agindua. Token bat jasoko duzu urrats honen ostean.\n3, Hasi txat bat zure bot berriarekin. Bidali mezu bat aurreko barra batekin (\"/\") \"/kaixo\" bezala gaitzeko.\n"),
("cancel-2fa-confirm-tip", "Ziur 2FA bertan behera utzi nahi duzula?"),
("cancel-bot-confirm-tip", "Ziur Telegram bota bertan behera utzi nahi duzula?"),
("About RustDesk", "RustDesk-i buruz"),
("Send clipboard keystrokes", "Bidali arbelaren tekla-sakatzeak"),
("network_error_tip", "Egiaztatu zure sareko konexioa eta egin klik berriz saiatzeko."),
("Unlock with PIN", "Desblokeatu PINarekin"),
("Requires at least {} characters", "Gutxienez {} karaktere behar ditu"),
("Wrong PIN", "PIN okerra"),
("Set PIN", "Ezarri PINa"),
("Enable trusted devices", "Gaitu gailu fidagarriak"),
("Manage trusted devices", "Kudeatu gailu fidagarriak"),
("Platform", "Plataforma"),
("Days remaining", "Geratzen diren egunak"),
("enable-trusted-devices-tip", "Saltatu 2FA egiaztapena gailu fidagarrietan"),
("Parent directory", "Direktorio gurasoa"),
("Resume", "Berrekin"),
("Invalid file name", "Fitxategi-izen baliogabea"),
("one-way-file-transfer-tip", "Norabide bakarreko fitxategi-transferentzia gaituta dago kontrolatutako aldean."),
("Authentication Required", "Autentifikazioa beharrezkoa da"),
("Authenticate", "Autentifikatu"),
("web_id_input_tip", "Zerbitzari berean dagoen ID bat sar dezakezu; IP bidezko sarbide zuzena ez da onartzen web-bezeroan.\nBeste zerbitzari batean dagoen gailu batera sartu nahi baduzu, erantsi zerbitzariaren helbidea (<id>@<server_address>?key=<key_value>), adibidez,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nZerbitzari publiko batean dagoen gailu batera sartu nahi baduzu, sartu \"<id>@public\"; gakoa ez da beharrezkoa zerbitzari publikoarentzat."),
("Download", "Deskargatu"),
("Upload folder", "Igo karpeta"),
("Upload files", "Igo fitxategiak"),
("Clipboard is synchronized", "Arbela sinkronizatuta dago"),
("Update client clipboard", "Eguneratu bezeroaren arbela"),
("Untagged", "Etiketarik gabe"),
("new-version-of-{}-tip", "{}(r)en bertsio berri bat dago eskuragarri"),
("Accessible devices", "Gailu eskuragarriak"),
("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", "Mesedez, eguneratu RustDesk bezeroa {} bertsiora edo berriagoa urruneko aldean!"),
("d3d_render_tip", "D3D errendatzea gaituta dagoenean, baliteke urruneko kontroleko pantaila beltza agertzea makina batzuetan."),
("Use D3D rendering", "Erabili D3D errendatzea"),
("Printer", "Inprimagailua"),
("printer-os-requirement-tip", "Inprimagailuaren irteerako funtzioak Windows 10 edo berriagoa behar du."),
("printer-requires-installed-{}-client-tip", "Urruneko inprimatzea erabiltzeko, {} gailu honetan instalatu behar da."),
("printer-{}-not-installed-tip", "{} inprimagailua ez dago instalatuta."),
("printer-{}-ready-tip", "{} inprimagailua instalatuta eta erabiltzeko prest dago."),
("Install {} Printer", "Instalatu {} inprimagailua"),
("Outgoing Print Jobs", "Irteerako inprimatze-lanak"),
("Incoming Print Jobs", "Sarrerako inprimatze-lanak"),
("Incoming Print Job", "Sarrerako inprimatze-lana"),
("use-the-default-printer-tip", "Erabili inprimagailu lehenetsia"),
("use-the-selected-printer-tip", "Erabili hautatutako inprimagailua"),
("auto-print-tip", "Inprimatu automatikoki hautatutako inprimagailua erabiliz."),
("print-incoming-job-confirm-tip", "Urrunetik inprimatze-lan bat jaso duzu. Zure aldean exekutatu nahi duzu?"),
("remote-printing-disallowed-tile-tip", "Urruneko inprimatzea debekatuta"),
("remote-printing-disallowed-text-tip", "Kontrolatutako aldearen baimen-ezarpenek urruneko inprimatzea ukatzen dute."),
("save-settings-tip", "Gorde ezarpenak"),
("dont-show-again-tip", "Ez erakutsi hau berriro"),
("Take screenshot", "Atera pantaila-argazkia"),
("Taking screenshot", "Pantaila-argazkia ateratzen"),
("screenshot-merged-screen-not-supported-tip", "Pantaila anitzen pantaila-argazkiak bateratzea ez da onartzen une honetan. Aldatu pantaila bakarrera eta saiatu berriro."),
("screenshot-action-tip", "Hautatu pantaila-argazkiarekin nola jarraitu."),
("Save as", "Gorde honela"),
("Copy to clipboard", "Kopiatu arbelera"),
("Enable remote printer", "Gaitu urruneko inprimagailua"),
("Downloading {}", "{} deskargatzen"),
("{} Update", "{} eguneratzea"),
("{}-to-update-tip", "{} orain itxiko da eta bertsio berria instalatuko du."),
("download-new-version-failed-tip", "Deskargak huts egin du. Berriro saia zaitezke edo egin klik \"Deskargatu\" botoian argitalpen-orritik deskargatu eta eskuz eguneratzeko."),
("Auto update", "Eguneratze automatikoa"),
("update-failed-check-msi-tip", "Instalazio-metodoaren egiaztapenak huts egin du. Egin klik \"Deskargatu\" botoian argitalpen-orritik deskargatu eta eskuz eguneratzeko."),
("websocket_tip", "WebSocket erabiltzean, errele-konexioak soilik onartzen dira."),
("Use WebSocket", "Erabili WebSocket"),
("Trackpad speed", "Trackpad-aren abiadura"),
("Default trackpad speed", "Trackpad-aren abiadura lehenetsia"),
("Numeric one-time password", "Behin-behineko pasahitz numerikoa"),
("Enable IPv6 P2P connection", "Gaitu IPv6 P2P konexioa"),
("Enable UDP hole punching", "Gaitu UDP zulo-egitea"),
("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", "Ikusi kamera"),
("Enable camera", "Gaitu kamera"),
("No cameras", "Kamerarik ez"),
("view_camera_unsupported_tip", "Urruneko gailuak ez du kamera ikustea onartzen."),
("Terminal", "Terminala"),
("Enable terminal", "Gaitu terminala"),
("New tab", "Fitxa berria"),
("Keep terminal sessions on disconnect", "Mantendu terminaleko saioak deskonektatzean"),
("Terminal (Run as administrator)", "Terminala (Exekutatu administratzaile gisa)"),
("terminal-admin-login-tip", "Sartu kontrolatutako aldearen administratzailearen erabiltzaile-izena eta pasahitza."),
("Failed to get user token.", "Ezin izan da erabiltzailearen tokena lortu."),
("Incorrect username or password.", "Erabiltzaile-izen edo pasahitz okerra."),
("The user is not an administrator.", "Erabiltzailea ez da administratzailea."),
("Failed to check if the user is an administrator.", "Ezin izan da egiaztatu erabiltzailea administratzailea den."),
("Supported only in the installed version.", "Instalatutako bertsioan soilik onartzen da."),
("elevation_username_tip", "Sartu erabiltzaile-izena edo domeinua\\erabiltzaile-izena"),
("Preparing for installation ...", "Instalaziorako prestatzen ..."),
("Show my cursor", "Erakutsi nire kurtsorea"),
("Scale custom", "Eskala pertsonalizatua"),
("Custom scale slider", "Eskala pertsonalizatuaren graduatzailea"),
("Decrease", "Txikiagotu"),
("Increase", "Handiagotu"),
("Show virtual mouse", "Erakutsi sagu birtuala"),
("Virtual mouse size", "Sagu birtualaren tamaina"),
("Small", "Txikia"),
("Large", "Handia"),
("Show virtual joystick", "Erakutsi joystick birtuala"),
("Edit note", "Editatu nota"),
("Alias", "Aliasa"),
("ScrollEdge", "ScrollEdge"),
("Allow insecure TLS fallback", "Onartu TLS ordezko ez-segurua"),
("allow-insecure-tls-fallback-tip", "Modu lehenetsian, RustDesk-ek zerbitzariaren ziurtagiria egiaztatzen du TLS erabiltzen duten protokoloetarako.\nAukera hau gaituta dagoela, RustDesk-ek egiaztapen-urratsa saltatzera joko du eta aurrera egingo du egiaztapenak huts eginez gero."),
("Disable UDP", "Desgaitu UDP"),
("disable-udp-tip", "TCP soilik erabiliko den kontrolatzen du.\nAukera hau gaituta dagoenean, RustDesk-ek ez du gehiago UDP 21116 erabiliko; horren ordez, TCP 21116 erabiliko da."),
("server-oss-not-support-tip", "OHARRA: RustDesk zerbitzariaren OSS bertsioak ez du ezaugarri hau barne hartzen."),
("input note here", "sartu nota hemen"),
("note-at-conn-end-tip", "Eskatu nota konexioaren amaieran"),
("Show terminal extra keys", "Erakutsi terminaleko tekla gehigarriak"),
("Relative mouse mode", "Sagu erlatiboaren modua"),
("rel-mouse-not-supported-peer-tip", "Konektatutako parekideak ez du sagu erlatiboaren modua onartzen."),
("rel-mouse-not-ready-tip", "Sagu erlatiboaren modua ez dago oraindik prest. Saiatu berriro."),
("rel-mouse-lock-failed-tip", "Ezin izan da kurtsorea blokeatu. Sagu erlatiboaren modua desgaitu egin da."),
("rel-mouse-exit-{}-tip", "Sakatu {} irteteko."),
("rel-mouse-permission-lost-tip", "Teklatuaren baimena baliogabetu da. Sagu erlatiboaren modua desgaitu egin da."),
("Changelog", "Aldaketen erregistroa"),
("keep-awake-during-outgoing-sessions-label", "Mantendu pantaila piztuta irteerako saioetan"),
("keep-awake-during-incoming-sessions-label", "Mantendu pantaila piztuta sarrerako saioetan"),
("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", ""),
("Continue with {}", "{} honekin jarraitu"),
("Display Name", "Bistaratze-izena"),
("password-hidden-tip", "Betiko pasahitza ezarrita dago (ezkutua)."),
("preset-password-in-use-tip", "Aurrez ezarritako pasahitza erabiltzen ari da une honetan."),
("Enable privacy mode", "Gaitu pribatutasun modua"),
("allow-remote-toolbar-docking-any-edge", "Onartu urruneko tresna-barra leihoaren edozein ertzetan atrakatzea"),
("API Token", "API tokena"),
("Deploy", "Hedatu"),
("Custom ID (optional)", "ID pertsonalizatua (aukerakoa)"),
("server_requires_deployment_tip", "Zerbitzariak gailu hau esplizituki hedatzea eskatzen du. Hedatu orain?"),
("The server does not require explicit deployment.", "Zerbitzariak ez du hedapen esplizitua eskatzen."),
("Unknown response.", "Erantzun ezezaguna."),
("wayland-keyboard-input-disabled-tip", "Onartu teklatuko sarrera?"),
("wayland-keyboard-input-consent-tip", "Urruneko ordenagailu honetan idazten duzuna (pasahitzak barne) bertako beste aplikazio batzuek irakur dezakete."),
("wayland-keyboard-input-applies-to-tip", "Aukera hau honi aplikatzen zaio:"),
("wayland-soft-keyboard-input-label", "Teklatu birtualaren sarrera"),
("wayland-keyboard-input-reset-choice-tip", "Berrezarri teklatuko sarreraren aukera"),
("remember-wayland-keyboard-choice-tip", "Ez galdetu berriro urruneko ordenagailu honetarako"),
("Why this happens", "Zergatik gertatzen den hau"),
("Display Name", ""),
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
].iter().cloned().collect();
}

View File

@@ -379,7 +379,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Screen Share", "اشتراک گذاری صفحه"),
("ubuntu-21-04-required", "نیازمند اوبونتو نسخه 21.04 یا بالاتر است Wayland"),
("wayland-requires-higher-linux-version", "استفاده کنید و یا سیستم عامل خود را تغییر دهید X11 نیازمند نسخه بالاتری از توزیع لینوکس است. لطفا از دسکتاپ با سیستم"),
("xdp-portal-unavailable", "ضبط صفحه Wayland ناموفق بود. ممکن است XDG Desktop Portal از کار افتاده یا در دسترس نباشد. با اجرای دستور `systemctl --user restart xdg-desktop-portal` آن را مجدداً راه‌اندازی کنید."),
("xdp-portal-unavailable", ""),
("JumpLink", "چشم انداز"),
("Please Select the screen to be shared(Operate on the peer side).", "لطفاً صفحه‌ای را برای اشتراک‌گذاری انتخاب کنید (در سمت همتا به همتا کار کنید)."),
("Show RustDesk", "RustDesk نمایش"),
@@ -721,7 +721,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", "نمایش جوی‌استیک مجازی"),
("Edit note", "ویرایش یادداشت"),
("Alias", "نام مستعار"),
("ScrollEdge", "ScrollEdge"),
("ScrollEdge", ""),
("Allow insecure TLS fallback", "استفاده از TLS غیر امن در ارتباط"),
("allow-insecure-tls-fallback-tip", "به‌طور پیش‌فرض، RustDesk گواهی سرور را برای پروتکل‌ها با استفاده از TLS تأیید می‌کند.\nبا فعال بودن این گزینه، RustDesk دوباره مرحله تأیید را رد می‌کند و در صورت عدم موفقیت تأیید ادامه می‌دهد."),
("Disable UDP", "UDP غیر فعال کردن"),
@@ -729,34 +729,19 @@ 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", "نمایش کلیدهای اضافی ترمینال"),
("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", "رمز عبور از پیش تعیین‌شده در حال حاضر در حال استفاده است."),
("Enable privacy mode", "فعال‌سازی حالت حریم خصوصی"),
("allow-remote-toolbar-docking-any-edge", "اجازه اتصال نوار ابزار از راه دور به هر لبه پنجره"),
("API Token", "توکن API"),
("Deploy", "استقرار"),
("Custom ID (optional)", "شناسه سفارشی (اختیاری)"),
("server_requires_deployment_tip", "سرور نیاز دارد که این دستگاه به‌طور صریح مستقر شود. اکنون مستقر شود؟"),
("The server does not require explicit deployment.", "سرور به استقرار صریح نیاز ندارد."),
("Unknown response.", "پاسخ ناشناخته."),
("wayland-keyboard-input-disabled-tip", "اجازه ورودی صفحه کلید داده شود؟"),
("wayland-keyboard-input-consent-tip", "آنچه روی این رایانه از راه دور تایپ می‌کنید (از جمله رمزهای عبور) ممکن است توسط سایر برنامه‌های روی آن خوانده شود."),
("wayland-keyboard-input-applies-to-tip", "این انتخاب برای موارد زیر اعمال می‌شود:"),
("wayland-soft-keyboard-input-label", "ورودی صفحه کلید نرم‌افزاری"),
("wayland-keyboard-input-reset-choice-tip", "بازنشانی انتخاب ورودی صفحه کلید"),
("remember-wayland-keyboard-choice-tip", "برای این رایانه از راه دور دوباره نپرس"),
("Why this happens", "چرا این اتفاق می‌افتد"),
("Display Name", ""),
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
].iter().cloned().collect();
}

View File

@@ -379,7 +379,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Screen Share", "Näytönjako"),
("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", "Waylandin näytön kaappaus epäonnistui. XDG Desktop Portal on saattanut kaatua tai ei ole käytettävissä. Yritä käynnistää se uudelleen komennolla `systemctl --user restart xdg-desktop-portal`."),
("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"),
@@ -721,42 +721,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Show virtual joystick", "Näytä virtuaalinen ohjain"),
("Edit note", "Muokkaa muistiinpanoa"),
("Alias", "Alias"),
("ScrollEdge", "ScrollEdge"),
("Allow insecure TLS fallback", "Salli turvaton TLS-varajärjestely"),
("allow-insecure-tls-fallback-tip", "Oletuksena RustDesk vahvistaa palvelimen varmenteen TLS:ää käyttävissä protokollissa.\nKun tämä asetus on käytössä, RustDesk ohittaa vahvistusvaiheen ja jatkaa toimintaa, jos vahvistus epäonnistuu."),
("Disable UDP", "Poista UDP käytöstä"),
("disable-udp-tip", "Määrittää, käytetäänkö vain TCP:tä.\nKun tämä asetus on käytössä, RustDesk ei enää käytä UDP-porttia 21116, vaan sen sijaan käytetään TCP-porttia 21116."),
("server-oss-not-support-tip", "HUOMAUTUS: RustDesk-palvelimen OSS ei sisällä tätä ominaisuutta."),
("input note here", "syötä muistiinpano tähän"),
("note-at-conn-end-tip", "Kysy muistiinpanoa yhteyden päättyessä"),
("Show terminal extra keys", "Näytä päätteen lisänäppäimet"),
("Relative mouse mode", "Suhteellinen hiiritila"),
("rel-mouse-not-supported-peer-tip", "Yhdistetty etäpää ei tue suhteellista hiiritilaa."),
("rel-mouse-not-ready-tip", "Suhteellinen hiiritila ei ole vielä valmis. Yritä uudelleen."),
("rel-mouse-lock-failed-tip", "Kursorin lukitseminen epäonnistui. Suhteellinen hiiritila on poistettu käytöstä."),
("rel-mouse-exit-{}-tip", "Poistu painamalla {}."),
("rel-mouse-permission-lost-tip", "Näppäimistöoikeus peruutettiin. Suhteellinen hiiritila on poistettu käytöstä."),
("Changelog", "Muutosloki"),
("keep-awake-during-outgoing-sessions-label", "Pidä näyttö hereillä lähtevien istuntojen aikana"),
("keep-awake-during-incoming-sessions-label", "Pidä näyttö hereillä saapuvien istuntojen aikana"),
("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", ""),
("Continue with {}", "Jatka käyttäen {}"),
("Display Name", "Näyttönimi"),
("password-hidden-tip", "Pysyvä salasana on asetettu (piilotettu)."),
("preset-password-in-use-tip", "Esiasetettu salasana on parhaillaan käytössä."),
("Enable privacy mode", "Ota yksityisyystila käyttöön"),
("allow-remote-toolbar-docking-any-edge", "Salli etätyökalupalkin kiinnittäminen mihin tahansa ikkunan reunaan"),
("API Token", "API-tunnus"),
("Deploy", "Ota käyttöön"),
("Custom ID (optional)", "Mukautettu tunnus (valinnainen)"),
("server_requires_deployment_tip", "Palvelin edellyttää, että tämä laite otetaan käyttöön nimenomaisesti. Otetaanko käyttöön nyt?"),
("The server does not require explicit deployment.", "Palvelin ei edellytä nimenomaista käyttöönottoa."),
("Unknown response.", "Tuntematon vastaus."),
("wayland-keyboard-input-disabled-tip", "Sallitaanko näppäimistösyöte?"),
("wayland-keyboard-input-consent-tip", "Muut tämän etätietokoneen sovellukset voivat lukea sen, mitä kirjoitat (myös salasanat)."),
("wayland-keyboard-input-applies-to-tip", "Tämä valinta koskee:"),
("wayland-soft-keyboard-input-label", "Virtuaalinäppäimistön syöte"),
("wayland-keyboard-input-reset-choice-tip", "Nollaa näppäimistösyötteen valinta"),
("remember-wayland-keyboard-choice-tip", "Älä kysy uudelleen tältä etätietokoneelta"),
("Why this happens", "Miksi näin tapahtuu"),
("Display Name", ""),
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
].iter().cloned().collect();
}

View File

@@ -743,20 +743,5 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Display Name", "Nom daffichage"),
("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", "Autoriser lancrage de la barre doutils à distance sur nimporte quel bord de la fenêtre"),
("API Token", "Jeton API"),
("Deploy", "Déployer"),
("Custom ID (optional)", "ID personnalisé (facultatif)"),
("server_requires_deployment_tip", "Le serveur requiert que cet appareil soit déployé explicitement. Déployer maintenant ?"),
("The server does not require explicit deployment.", "Le serveur ne requiert pas de déploiement explicite."),
("Unknown response.", "Réponse inconnue."),
("wayland-keyboard-input-disabled-tip", "Autoriser la saisie au clavier ?"),
("wayland-keyboard-input-consent-tip", "Les saisies sur cet appareil distant (mots de passe y compris) pourraient être lues par dautres applications."),
("wayland-keyboard-input-applies-to-tip", "Ce choix sapplique à :"),
("wayland-soft-keyboard-input-label", "Saisie au clavier virtuel"),
("wayland-keyboard-input-reset-choice-tip", "Réinitialiser le choix de la saisie au clavier"),
("remember-wayland-keyboard-choice-tip", "Ne plus demander pour cet appareil distant"),
("Why this happens", "Pourquoi cela se produit"),
].iter().cloned().collect();
}

View File

@@ -237,7 +237,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Empty", "ცარიელი"),
("Invalid folder name", "არასწორი საქაღალდის სახელი"),
("Socks5 Proxy", "SOCKS5-პროქსი"),
("Socks5/Http(s) Proxy", "Socks5/Http(s) პროქსი"),
("Socks5/Http(s) Proxy", ""),
("Discovered", "ნაპოვნია"),
("install_daemon_tip", "ჩატვირთვისას გასაშვებად საჭიროა სისტემური სერვისის დაყენება"),
("Remote ID", "დაშორებული ID"),
@@ -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", "Wayland-ის ეკრანის ჩაწერა ვერ მოხერხდა. XDG Desktop Portal შესაძლოა ავარიულად დასრულდა ან მიუწვდომელია. სცადეთ მისი გადატვირთვა ბრძანებით `systemctl --user restart xdg-desktop-portal`."),
("xdp-portal-unavailable", ""),
("JumpLink", "ნახვა"),
("Please Select the screen to be shared(Operate on the peer side).", "აირჩიეთ ეკრანი გასაზიარებლად (იმუშავეთ პარტნიორის მხარეს)."),
("Show RustDesk", "RustDesk-ის ჩვენება"),
@@ -672,91 +672,76 @@ 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", "მიმდინარეობს ეკრანის სურათის გადაღება"),
("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-ის ჩართვა"),
("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", "შეიყვანეთ მომხმარებლის სახელი ან domain\\username"),
("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", "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", "ტერმინალის დამატებითი ღილაკების ჩვენება"),
("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", "ეკრანის ღვიძილის რეჟიმში შენარჩუნება შემომავალი სესიების დროს"),
("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", ""),
("Continue with {}", "{}-ით გაგრძელება"),
("Display Name", "საჩვენებელი სახელი"),
("password-hidden-tip", "მუდმივი პაროლი დაყენებულია (დამალული)."),
("preset-password-in-use-tip", "ამჟამად გამოიყენება წინასწარ დაყენებული პაროლი."),
("Enable privacy mode", "კონფიდენციალურობის რეჟიმის ჩართვა"),
("allow-remote-toolbar-docking-any-edge", "დისტანციური ხელსაწყოთა პანელის მიმაგრების დაშვება ფანჯრის ნებისმიერ კიდეზე"),
("API Token", "API ტოკენი"),
("Deploy", "გაშლა"),
("Custom ID (optional)", "მორგებული ID (არასავალდებულო)"),
("server_requires_deployment_tip", "სერვერი მოითხოვს ამ მოწყობილობის ცალსახად გაშლას. გავშალოთ ახლა?"),
("The server does not require explicit deployment.", "სერვერი არ მოითხოვს ცალსახა გაშლას."),
("Unknown response.", "უცნობი პასუხი."),
("wayland-keyboard-input-disabled-tip", "დავუშვათ კლავიატურის შეყვანა?"),
("wayland-keyboard-input-consent-tip", "ის, რასაც ამ დისტანციურ კომპიუტერზე აკრებთ (პაროლების ჩათვლით), შესაძლოა წაიკითხონ მასზე არსებულმა სხვა აპლიკაციებმა."),
("wayland-keyboard-input-applies-to-tip", "ეს არჩევანი ვრცელდება:"),
("wayland-soft-keyboard-input-label", "პროგრამული კლავიატურის შეყვანა"),
("wayland-keyboard-input-reset-choice-tip", "კლავიატურის შეყვანის არჩევანის ჩამოყრა"),
("remember-wayland-keyboard-choice-tip", "აღარ მკითხო ამ დისტანციური კომპიუტერისთვის"),
("Why this happens", "რატომ ხდება ეს"),
("Display Name", ""),
("password-hidden-tip", ""),
("preset-password-in-use-tip", ""),
].iter().cloned().collect();
}

View File

@@ -654,7 +654,6 @@ 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", "D3D રેન્ડરિંગનો ઉપયોગ કરો"),
("Printer", "પ્રિન્ટર"),
("printer-os-requirement-tip", "પ્રિન્ટિંગ માટે Windows જરૂરી છે."),
("printer-requires-installed-{}-client-tip", "આ માટે {} ક્લાયન્ટ ઇન્સ્ટોલ હોવું જોઈએ."),
@@ -743,20 +742,5 @@ 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", "રિમોટ ટૂલબારને કોઈપણ વિન્ડો કિનારી પર ડોક કરવાની મંજૂરી આપો"),
("API Token", "API ટોકન"),
("Deploy", "ડિપ્લોય કરો"),
("Custom ID (optional)", "કસ્ટમ ID (વૈકલ્પિક)"),
("server_requires_deployment_tip", "સર્વરને આ ઉપકરણ સ્પષ્ટપણે ડિપ્લોય કરવાની જરૂર છે. હવે ડિપ્લોય કરવું છે?"),
("The server does not require explicit deployment.", "સર્વરને સ્પષ્ટ ડિપ્લોયમેન્ટની જરૂર નથી."),
("Unknown response.", "અજાણ્યો પ્રતિસાદ."),
("wayland-keyboard-input-disabled-tip", "કીબોર્ડ ઇનપુટની મંજૂરી આપવી છે?"),
("wayland-keyboard-input-consent-tip", "આ રિમોટ કમ્પ્યુટર પર તમે જે ટાઇપ કરો છો (પાસવર્ડ સહિત) તે તેના પરની અન્ય એપ્સ વાંચી શકે છે."),
("wayland-keyboard-input-applies-to-tip", "આ પસંદગી આના પર લાગુ થાય છે:"),
("wayland-soft-keyboard-input-label", "સોફ્ટ કીબોર્ડ ઇનપુટ"),
("wayland-keyboard-input-reset-choice-tip", "કીબોર્ડ ઇનપુટ પસંદગી રિસેટ કરો"),
("remember-wayland-keyboard-choice-tip", "આ રિમોટ કમ્પ્યુટર માટે ફરીથી પૂછશો નહીં"),
("Why this happens", "આવું શા માટે થાય છે"),
].iter().cloned().collect();
}

Some files were not shown because too many files have changed in this diff Show More