Compare commits

..

18 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
1274935106 Fix macOS build: Remove @available check causing linker error
The @available check in GetDisplayName was causing the linker to look for
__isPlatformVersionAtLeast symbol which is not available when targeting
macOS 10.14. Since this function is only used for logging, we simplify it
to return "Unknown" for all displays, avoiding the runtime availability check.

Co-authored-by: rustdesk <71636191+rustdesk@users.noreply.github.com>
2026-01-28 08:49:42 +00:00
copilot-swe-agent[bot]
85b3ed5276 Initial plan 2026-01-28 08:47:32 +00:00
twprh
5f3ceef592 Update de.rs (#14139)
zum Zeitpunkt der Anzeige ist der Datenschutz aktiviert bzw. schon beendet.

alternativ ginge auch:

Datenschutzmodus wurde aktiviert  
bzw.
 Datenschutzmodus wurde beendet
2026-01-28 15:16:27 +08:00
Lynilia
1a90e6b6c7 Update fr.rs (#14151) 2026-01-28 15:16:06 +08:00
John Fowler
f112d097dc Replacing incorrect quotation marks (#14144)
* Update Hungarian translations in hu.rs

Translation of new strings and some fixes.
John Fowler.

* Escape quotes in Hungarian language strings

Replacing Hungarian quotation marks

* Update Hungarian translations for various terms

Upload a new translation (hu.rs) file.
2026-01-28 15:15:29 +08:00
ThallesWS
45cab7f808 fix issue: #13911 'Double Click' bug on iPad with Magic Mouse (#14086)
* fix issue: #13911 'Double Click' bug on iPad with Magic Mouse

* remote_input.dart comments - gestures.dart organization and clean states of all interrupted gestures
2026-01-28 15:14:06 +08:00
fufesou
216ec9d52b fix(terminal): ios delete (#14147)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-01-28 15:12:42 +08:00
fufesou
56a8f6b97b fix(iOS): Unexpected mouse movement to (0,0) on idle (#14180)
Signed-off-by: fufesou <linlong1266@gmail.com>
2026-01-28 15:11:44 +08:00
Bin Li
c76d10a438 feat(macos): initial privacy mode support [a simple try] (#14102)
* feat(macos): add privacy mode support for macOS

## Summary
Add privacy mode functionality for macOS platform, allowing remote
desktop sessions to hide the screen content from local users.

## Changes

### Core Implementation (src/platform/macos.mm)
- Implement screen blackout using CGDisplayGammaTable API
- Implement input blocking using CGEventTap to intercept keyboard/mouse
- Store and restore original gamma values for proper cleanup

### Privacy Mode Integration (src/privacy_mode.rs, src/privacy_mode/macos.rs)
- Add macOS privacy mode implementation with PrivacyMode trait
- Register macOS privacy mode in PRIVACY_MODE_CREATOR
- Set DEFAULT_PRIVACY_MODE_IMPL for macOS platform
- Implement get_supported_privacy_mode_impl() for macOS

### Connection Handling (src/server/connection.rs)
- Add supported_privacy_mode_impl to platform_additions for macOS
- Enable privacy mode toggle in client UI when connecting via LAN IP

### Localization (src/lang/*.rs)
- Add "privacy_mode_impl_macos_tip" translation for en/cn/tw

## Safety & Security
- Implements Drop trait to ensure cleanup on normal exit
- macOS system automatically restores gamma table on process termination
- CGEventTap is automatically released when process terminates
- Tested with SIGKILL to verify crash recovery

## Testing
- Verified privacy mode toggle works via both ID and LAN IP connection
- Verified screen recovery after process crash (kill -9)
- Verified input restoration after process termination

* refactor: use existing 'Privacy mode' translation key

* refactor: rename gamma channel variables for better readability - rename r/g/b to red/green/blue to avoid variable shadowing confusion

* fix: add error handling for gamma table restoration with fallback to system reset

* fix: add error handling for CGEventTapCreate failure in privacy mode

* fix: only set display to black if original gamma was saved successfully

* fix: add error handling for CGSetDisplayTransferByTable when setting display to black

* fix: improve event tap callback to properly distinguish remote input from local input

* fix: missing macos.rs

* Fix: Add display validation before restoring gamma values

* Fix: Add mutex lock for thread safety in MacSetPrivacyMode

* Fix: Handle return values and add missing mouse events in macos privacy mode

* fix: only set conn_id after privacy mode is successfully turned on

* fix: reimplement privacy mode with stable display identification

Address code review concern: original gamma values stored with DisplayID
as key could become stale if display list changes between privacy mode
activations (e.g., display reconnected with different ID).

Solution:
- Use UUID instead of DisplayID as storage key (stable across reconnections)
- Clear g_originalGammas when privacy mode is turned off
- Register CGDisplayReconfigurationCallback to handle hot-plug events
- Validate display state via FindDisplayIdByUUID() before restoration

Key features:
- UUID-based display identification (stable across reconnections)
- Hot-plug support via CGDisplayReconfigurationCallback
- EventTap auto re-enable on system timeout
- Fallback to CGDisplayRestoreColorSyncSettings() for recovery
- Detailed error logging with display name/ID/UUID

* fix: ensure EventTap runs on main thread and improve gamma restore error handling

- Add SetupEventTapOnMainThread() to create EventTap on main thread using dispatch_sync, avoiding potential issues when called from background threads

- Add TeardownEventTapOnMainThread() for consistent cleanup on main thread

- Check [NSThread isMainThread] to avoid deadlock when already on main thread

- Add error tracking for gamma restoration during cleanup

- Use CGDisplayRestoreColorSyncSettings() as fallback when individual gamma restoration fails

* fix: remove invalid eventMask bits that caused undefined behavior in input blocking

* fix: address code review comments for macos privacy mode implementation

Changes to src/privacy_mode/macos.rs:
- Add check_on_conn_id() in turn_on_privacy() to prevent duplicate activation
- Add check_off_conn_id() in turn_off_privacy() to validate connection ID
- Add self.conn_id = 0 in clear() to reset connection state

Changes to src/platform/macos.mm:
- Add link comment for ENIGO_INPUT_EXTRA_VALUE referencing libs/enigo/src/macos/macos_impl.rs
- Fix NSLog format string mismatch (5 placeholders vs 4 values)
- Make ApplyBlackoutToDisplay() return bool for proper error handling
- Return false when UUID is empty since privacy mode requires ALL displays
- Add else branches with logging for:
  - CGGetDisplayTransferByTable failures
  - Zero gamma table capacity (not supported)
  - Zero blackout capacity
- Remove unused g_uuidToDisplayId variable (was only written, never read)

* fix(macos): add early return with privacy mode exit on display hotplug failures

Why large-scale changes are needed:

The code review suggested adding early return when errors occur in
DisplayReconfigurationCallback. However, simply returning early is not
enough - when a newly connected display cannot be blacked out, we must
exit privacy mode entirely to maintain security guarantees.

The challenge is that DisplayReconfigurationCallback already holds
g_privacyModeMutex, so calling MacSetPrivacyMode(false) directly would
cause a deadlock. This necessitated:

1. Extract TurnOffPrivacyModeInternal() - a lock-free internal function
   that can be safely called from within the callback
2. Refactor MacSetPrivacyMode(false) branch to use this internal function
3. Add early returns with TurnOffPrivacyModeInternal() calls at each
   failure point in DisplayReconfigurationCallback

Changes in DisplayReconfigurationCallback:
- UUID empty: log + exit privacy mode + early return
- Gamma table capacity zero: log + exit privacy mode + early return
- CGGetDisplayTransferByTable fails: log + exit privacy mode + early return
- ApplyBlackoutToDisplay fails: log + exit privacy mode + early return

* fix(macos): address code review feedback and improve privacy mode stability

Code Review Fixes:
- Add detailed comments for potential deadlock scenarios in dispatch_sync
  with g_privacyModeMutex (SetupEventTapOnMainThread/TeardownEventTapOnMainThread)
- Use async dispatch for privacy mode shutdown from DisplayReconfigurationCallback
  to avoid unregistering callback from within itself
- Extract RestoreAllGammas() helper function to reduce code duplication
- Fix Drop implementation in macos.rs to call self.clear() for consistency
- Add comment explaining why _state parameter is ignored on macOS
- Define DISPLAY_RECONFIG_MONITOR_DURATION_MS and GAMMA_CHECK_INTERVAL_MS constants
- Add gamma restoration when UUID retrieval fails during privacy mode activation

Privacy Mode Stability Improvements (Continuous Resolution Changes):
- Implement continuous gamma value monitoring with timer polling after display
  reconfiguration to handle rapid successive resolution changes
- Monitor gamma values every 200ms for 5 seconds after each resolution change
- Automatically reapply blackout if system (ColorSync) restores gamma
- Add IsDisplayBlackedOut() to detect if display gamma has been restored
- Use timestamp-based debouncing: monitoring period automatically extends
  when new reconfig events occur during active monitoring
- Ensure blackout remains effective even under continuous resolution changes
  where macOS may asynchronously restore gamma values multiple times

This ensures privacy mode remains stable and effective when users rapidly
change display resolution multiple times in succession.

---------

Co-authored-by: libin <libin.chat@outlook.com>
2026-01-27 16:38:37 +08:00
Alex Rijckaert
f05f2178e5 Update Dutch translations (#14136) 2026-01-26 14:16:21 +08:00
Hugo Breda
226d7417b2 PT-BR language update (#14135)
* PT-BR language update

@rustdesk
Please merge. Thanks

* Update ptbr.rs

* Update ptbr.rs

Please submit, i will get back soon and finish all other stuff.
2026-01-26 14:15:58 +08:00
bovirus
b0c8e65c6e Italian language update (#14129) 2026-01-26 14:15:45 +08:00
RustDesk
4ae577c3c4 Revert "Updated tr.rs (#14115)" (#14158)
This reverts commit 204e81a700.
2026-01-26 14:14:35 +08:00
bilimiyorum
204e81a700 Updated tr.rs (#14115)
Translation improvements have been made.
2026-01-26 14:11:58 +08:00
hatterp
1f35830570 Update pl.rs (#14112)
updated PL translation
2026-01-26 14:11:41 +08:00
VenusGirl❤
6b334f2977 Update ko.rs (#14110)
Update Korean
2026-01-25 16:37:34 +08:00
Mr-Update
0dc3c12aa5 Update de.rs (#14108) 2026-01-24 12:50:18 +08:00
RustDesk
ceffcce20e fix hide-tray=Y causing The application “RustDesk.app” is not open anymore. https://github.com/rustdesk/rustdesk/discussions/10210 (#14127) 2026-01-23 19:09:33 +08:00
16 changed files with 879 additions and 91 deletions

View File

@@ -25,6 +25,7 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
GestureDragStartCallback? onOneFingerPanStart;
GestureDragUpdateCallback? onOneFingerPanUpdate;
GestureDragEndCallback? onOneFingerPanEnd;
GestureDragCancelCallback? onOneFingerPanCancel;
// twoFingerScale : scale + pan event
GestureScaleStartCallback? onTwoFingerScaleStart;
@@ -169,6 +170,27 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
DragEndDetails _getDragEndDetails(ScaleEndDetails d) =>
DragEndDetails(velocity: d.velocity);
@override
void rejectGesture(int pointer) {
super.rejectGesture(pointer);
switch (_currentState) {
case GestureState.oneFingerPan:
if (onOneFingerPanCancel != null) {
onOneFingerPanCancel!();
}
break;
case GestureState.twoFingerScale:
// Reset scale state if needed, currently self-contained
break;
case GestureState.threeFingerVerticalDrag:
// Reset drag state if needed, currently self-contained
break;
default:
break;
}
_currentState = GestureState.none;
}
}
class HoldTapMoveGestureRecognizer extends GestureRecognizer {
@@ -717,6 +739,7 @@ RawGestureDetector getMixinGestureDetector({
GestureDragStartCallback? onOneFingerPanStart,
GestureDragUpdateCallback? onOneFingerPanUpdate,
GestureDragEndCallback? onOneFingerPanEnd,
GestureDragCancelCallback? onOneFingerPanCancel,
GestureScaleUpdateCallback? onTwoFingerScaleUpdate,
GestureScaleEndCallback? onTwoFingerScaleEnd,
GestureDragUpdateCallback? onThreeFingerVerticalDragUpdate,
@@ -765,6 +788,7 @@ RawGestureDetector getMixinGestureDetector({
..onOneFingerPanStart = onOneFingerPanStart
..onOneFingerPanUpdate = onOneFingerPanUpdate
..onOneFingerPanEnd = onOneFingerPanEnd
..onOneFingerPanCancel = onOneFingerPanCancel
..onTwoFingerScaleUpdate = onTwoFingerScaleUpdate
..onTwoFingerScaleEnd = onTwoFingerScaleEnd
..onThreeFingerVerticalDragUpdate = onThreeFingerVerticalDragUpdate;

View File

@@ -158,7 +158,8 @@ class _RawTouchGestureDetectorRegionState
final isMoved =
await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
if (isMoved) {
if (lastTapDownDetails != null) {
// If pan already handled 'down', don't send it again.
if (lastTapDownDetails != null && !_touchModePanStarted) {
await inputModel.tapDown(MouseButtons.left);
}
await inputModel.tapUp(MouseButtons.left);
@@ -424,6 +425,14 @@ class _RawTouchGestureDetectorRegionState
}
}
// Reset `_touchModePanStarted` if the one-finger pan gesture is cancelled
// or rejected by the gesture arena. Without this, the flag can remain
// stuck in the "started" state and cause issues such as the Magic Mouse
// double-click problem on iPad with magic mouse.
onOneFingerPanCancel() {
_touchModePanStarted = false;
}
// scale + pan event
onTwoFingerScaleStart(ScaleStartDetails d) {
_lastTapDownDetails = null;
@@ -557,6 +566,7 @@ class _RawTouchGestureDetectorRegionState
instance
..onOneFingerPanUpdate = onOneFingerPanUpdate
..onOneFingerPanEnd = onOneFingerPanEnd
..onOneFingerPanCancel = onOneFingerPanCancel
..onTwoFingerScaleStart = onTwoFingerScaleStart
..onTwoFingerScaleUpdate = onTwoFingerScaleUpdate
..onTwoFingerScaleEnd = onTwoFingerScaleEnd

View File

@@ -164,6 +164,13 @@ class _TerminalPageState extends State<TerminalPage>
autofocus: true,
textStyle: _getTerminalStyle(),
backgroundOpacity: 0.7,
// The following comment is from xterm.dart source code:
// Workaround to detect delete key for platforms and IMEs that do not
// emit a hardware delete event. Preferred on mobile platforms. [false] by
// default.
//
// Android works fine without this workaround.
deleteDetection: isIOS,
padding: _calculatePadding(heightPx),
onSecondaryTapDown: (details, offset) async {
final selection = _terminalModel.terminalController.selection;

View File

@@ -1048,6 +1048,14 @@ class InputModel {
if (isViewOnly && !showMyCursor) return;
if (e.kind != ui.PointerDeviceKind.mouse) return;
// May fix https://github.com/rustdesk/rustdesk/issues/13009
if (isIOS && e.synthesized && e.position == Offset.zero && e.buttons == 0) {
// iOS may emit a synthesized hover event at (0,0) when the mouse is disconnected.
// Ignore this event to prevent cursor jumping.
debugPrint('Ignored synthesized hover at (0,0) on iOS');
return;
}
// Only update pointer region when relative mouse mode is enabled.
// This avoids unnecessary tracking when not in relative mode.
if (_relativeMouse.enabled.value) {

View File

@@ -562,8 +562,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("id_input_tip", "Sie können eine ID, eine direkte IP oder eine Domäne mit einem Port (<domain>:<port>) eingeben.\nWenn Sie auf ein Gerät auf einem anderen Server zugreifen wollen, fügen Sie bitte die Serveradresse (<id>@<server_address>?key=<key_value>) hinzu, zum Beispiel\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nWenn Sie auf ein Gerät auf einem öffentlichen Server zugreifen wollen, geben Sie bitte \"<id>@public\" ein. Der Schlüssel wird für öffentliche Server nicht benötigt.\n\nWenn Sie bei der ersten Verbindung die Verwendung einer Relay-Verbindung erzwingen wollen, fügen Sie \"/r\" am Ende der ID hinzu, zum Beispiel \"9123456234/r\"."),
("privacy_mode_impl_mag_tip", "Modus 1"),
("privacy_mode_impl_virtual_display_tip", "Modus 2"),
("Enter privacy mode", "Datenschutzmodus aktivieren"),
("Exit privacy mode", "Datenschutzmodus beenden"),
("Enter privacy mode", "Datenschutzmodus aktiviert"),
("Exit privacy mode", "Datenschutzmodus beendet"),
("idd_not_support_under_win10_2004_tip", "Indirekter Grafiktreiber wird nicht unterstützt. Windows 10, Version 2004 oder neuer ist erforderlich."),
("input_source_1_tip", "Eingangsquelle 1"),
("input_source_2_tip", "Eingangsquelle 2"),
@@ -737,7 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("rel-mouse-exit-{}-tip", "Drücken Sie {} zum Beenden."),
("rel-mouse-permission-lost-tip", "Die Tastaturberechtigung wurde widerrufen. Der relative Mausmodus wurde deaktiviert."),
("Changelog", "Änderungsprotokoll"),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("keep-awake-during-outgoing-sessions-label", "Bildschirm während ausgehender Sitzungen aktiv halten"),
("keep-awake-during-incoming-sessions-label", "Bildschirm während eingehender Sitzungen aktiv halten"),
].iter().cloned().collect();
}

View File

@@ -106,7 +106,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Are you sure you want to delete this empty directory?", "Voulez-vous vraiment supprimer ce répertoire vide ?"),
("Are you sure you want to delete the file of this directory?", "Voulez-vous vraiment supprimer le fichier de ce répertoire ?"),
("Do this for all conflicts", "Appliquer à tous les conflits"),
("This is irreversible!", "Ceci est irréversible !"),
("This is irreversible!", "Cette action est irréversible !"),
("Deleting", "Suppression"),
("files", "fichiers"),
("Waiting", "En attente"),
@@ -737,7 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("rel-mouse-exit-{}-tip", "Appuyez sur {} pour quitter."),
("rel-mouse-permission-lost-tip", "Lautorisation de contrôle du clavier a été révoquée. Le mode souris relative a été désactivé."),
("Changelog", "Journal des modifications"),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("keep-awake-during-outgoing-sessions-label", "Maintenir lécran allumé lors des sessions sortantes"),
("keep-awake-during-incoming-sessions-label", "Maintenir lécran allumé lors des sessions entrantes"),
].iter().cloned().collect();
}

View File

@@ -149,7 +149,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Click to upgrade", "Kattintson ide a frissítés telepítéséhez"),
("Configure", "Beállítás"),
("config_acc", "A számítógép távoli vezérléséhez a RustDesknek hozzáférési jogokat kell adnia."),
("config_screen", "Ahhoz, hogy távolról hozzáférhessen a számítógépéhez, meg kell adnia a RustDesknek a Képernyőfelvétel jogosultságot."),
("config_screen", "Ahhoz, hogy távolról hozzáférhessen a számítógépéhez, meg kell adnia a RustDesknek a \"Képernyőfelvétel\" jogosultságot."),
("Installing ...", "Telepítés…"),
("Install", "Telepítés"),
("Installation", "Telepítés"),
@@ -276,13 +276,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Do you accept?", "Elfogadás?"),
("Open System Setting", "Rendszerbeállítások megnyitása"),
("How to get Android input permission?", "Hogyan állítható be az Androidos beviteli engedély?"),
("android_input_permission_tip1", "Ahhoz, hogy egy távoli eszköz vezérelhesse Android készülékét, engedélyeznie kell a RustDesk számára a Hozzáférhetőség szolgáltatás használatát."),
("android_input_permission_tip1", "Ahhoz, hogy egy távoli eszköz vezérelhesse Android készülékét, engedélyeznie kell a RustDesk számára a \"Hozzáférhetőség\" szolgáltatás használatát."),
("android_input_permission_tip2", "A következő rendszerbeállítások oldalon a letöltött alkalmazások menüponton belül, kapcsolja be a [RustDesk Input] szolgáltatást."),
("android_new_connection_tip", "Új kérés érkezett, mely vezérelni szeretné az eszközét"),
("android_service_will_start_tip", "A képernyőmegosztás aktiválása automatikusan elindítja a szolgáltatást, így más eszközök is vezérelhetik ezt az Android-eszközt."),
("android_stop_service_tip", "A szolgáltatás leállítása automatikusan szétkapcsol minden létező kapcsolatot."),
("android_version_audio_tip", "A jelenlegi Android verzió nem támogatja a hangrögzítést, frissítsen legalább Android 10-re, vagy egy újabb verzióra."),
("android_start_service_tip", "A képernyőmegosztó szolgáltatás elindításához koppintson a Kapcsolási szolgáltatás indítása gombra, vagy aktiválja a Képernyőfelvétel engedélyt."),
("android_start_service_tip", "A képernyőmegosztó szolgáltatás elindításához koppintson a \"Kapcsolási szolgáltatás indítása\" gombra, vagy aktiválja a \"Képernyőfelvétel\" engedélyt."),
("android_permission_may_not_change_tip", "A meglévő kapcsolatok engedélyei csak új kapcsolódás után módosulnak."),
("Account", "Fiók"),
("Overwrite", "Felülírás"),
@@ -408,15 +408,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Select local keyboard type", "Helyi billentyűzet típusának kiválasztása"),
("software_render_tip", "Ha Nvidia grafikus kártyát használ Linux alatt, és a távoli ablak a kapcsolat létrehozása után azonnal bezáródik, akkor a Nouveau nyílt forráskódú illesztőprogramra való váltás és a szoftveres leképezés alkalmazása segíthet. A szoftvert újra kell indítani."),
("Always use software rendering", "Mindig szoftveres leképezést használjon"),
("config_input", "Ahhoz, hogy a távoli asztalt a billentyűzettel vezérelhesse, a RustDesknek meg kell adnia a Bemenet figyelése jogosultságot."),
("config_microphone", "Ahhoz, hogy távolról beszélhessen, meg kell adnia a RustDesknek a Hangfelvétel jogosultságot."),
("config_input", "Ahhoz, hogy a távoli asztalt a billentyűzettel vezérelhesse, a RustDesknek meg kell adnia a \"Bemenet figyelése\" jogosultságot."),
("config_microphone", "Ahhoz, hogy távolról beszélhessen, meg kell adnia a RustDesknek a \"Hangfelvétel\" jogosultságot."),
("request_elevation_tip", "Akkor is kérhet megnövelt jogokat, ha valaki a partneroldalon van."),
("Wait", "Várjon"),
("Elevation Error", "Emelt szintű hozzáférési hiba"),
("Ask the remote user for authentication", "Hitelesítés kérése a távoli felhasználótól"),
("Choose this if the remote account is administrator", "Akkor válassza ezt, ha a távoli fiók rendszergazda"),
("Transmit the username and password of administrator", "Küldje el a rendszergazda felhasználónevét és jelszavát"),
("still_click_uac_tip", "A távoli felhasználónak továbbra is az „Igen” gombra kell kattintania a RustDesk UAC ablakában. Kattintson!"),
("still_click_uac_tip", "A távoli felhasználónak továbbra is az \"Igen\" gombra kell kattintania a RustDesk UAC ablakában. Kattintson!"),
("Request Elevation", "Emelt szintű jogok igénylése"),
("wait_accept_uac_tip", "Várjon, amíg a távoli felhasználó elfogadja az UAC párbeszédet."),
("Elevate successfully", "Emelt szintű jogok megadva"),
@@ -442,7 +442,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Voice call", "Hanghívás"),
("Text chat", "Szöveges csevegés"),
("Stop voice call", "Hanghívás leállítása"),
("relay_hint_tip", "Ha a közvetlen kapcsolat nem lehetséges, megpróbálhat kapcsolatot létesíteni egy továbbító-kiszolgálón keresztül.\nHa az első próbálkozáskor továbbító-kiszolgálón keresztüli kapcsolatot szeretne létrehozni, használhatja az „/r” utótagot. Az azonosítóhoz vagy a Mindig továbbító-kiszolgálón keresztül kapcsolódom opcióhoz a legutóbbi munkamenetek listájában, ha van ilyen."),
("relay_hint_tip", "Ha a közvetlen kapcsolat nem lehetséges, megpróbálhat kapcsolatot létesíteni egy továbbító-kiszolgálón keresztül.\nHa az első próbálkozáskor továbbító-kiszolgálón keresztüli kapcsolatot szeretne létrehozni, használhatja az \"/r\" utótagot. Az azonosítóhoz vagy a \"Mindig továbbító-kiszolgálón keresztül kapcsolódom\" opcióhoz a legutóbbi munkamenetek listájában, ha van ilyen."),
("Reconnect", "Újrakapcsolódás"),
("Codec", "Kodek"),
("Resolution", "Felbontás"),
@@ -490,7 +490,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Update", "Frissítés"),
("Enable", "Engedélyezés"),
("Disable", "Letiltás"),
("Options", "Beállítások"),
("Options", "Opciók"),
("resolution_original_tip", "Eredeti felbontás"),
("resolution_fit_local_tip", "Helyi felbontás beállítása"),
("resolution_custom_tip", "Testre szabható felbontás"),
@@ -559,7 +559,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Plug out all", "Kapcsolja ki az összeset"),
("True color (4:4:4)", "Valódi szín (4:4:4)"),
("Enable blocking user input", "Engedélyezze a felhasználói bevitel blokkolását"),
("id_input_tip", "Megadhat egy azonosítót, egy közvetlen IP-címet vagy egy tartományt egy porttal (<domain>:<port>).\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (<id>@<kiszolgáló_cím>?key=<key_value>), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a <id>@public lehetőséget. A kulcsra nincs szükség nyilvános kiszolgálók esetén.\n\nHa az első kapcsolathoz továbbító-kiszolgálón keresztüli kapcsolatot akar kényszeríteni, adja hozzá az „/r” az azonosítót a végén, például 9123456234/r."),
("id_input_tip", "Megadhat egy azonosítót, egy közvetlen IP-címet vagy egy tartományt egy porttal (<domain>:<port>).\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (<id>@<kiszolgáló_cím>?key=<key_value>), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a \"<id>@public\" lehetőséget. A kulcsra nincs szükség nyilvános kiszolgálók esetén.\n\nHa az első kapcsolathoz továbbító-kiszolgálón keresztüli kapcsolatot akar kényszeríteni, adja hozzá az \"/r\" az azonosítót a végén, például \"9123456234/r\"."),
("privacy_mode_impl_mag_tip", "1. mód"),
("privacy_mode_impl_virtual_display_tip", "2. mód"),
("Enter privacy mode", "Lépjen be az adatvédelmi módba"),
@@ -622,7 +622,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Power", "Főkapcsoló"),
("Telegram bot", "Telegram bot"),
("enable-bot-tip", "Ha aktiválja ezt a funkciót, akkor a 2FA-kódot a botjától kaphatja meg. Kapcsolati értesítésként is használható."),
("enable-bot-desc", "1. Nyisson csevegést @BotFather.\n2. Küldje el a /newbot parancsot. Miután ezt a lépést elvégezte, kap egy tokent.\n3. Indítson csevegést az újonnan létrehozott botjával. Küldjön egy olyan üzenetet, amely egy perjel („/”) kezdetű, pl. „/hello” az aktiváláshoz.\n"),
("enable-bot-desc", "1. Nyisson csevegést @BotFather.\n2. Küldje el a \"/newbot\" parancsot. Miután ezt a lépést elvégezte, kap egy tokent.\n3. Indítson csevegést az újonnan létrehozott botjával. Küldjön egy olyan üzenetet, amely egy perjel (\"/\") kezdetű, pl. \"/hello\" az aktiváláshoz.\n"),
("cancel-2fa-confirm-tip", "Biztosan vissza akarja vonni a 2FA-hitelesítést?"),
("cancel-bot-confirm-tip", "Biztosan le akarja mondani a Telegram botot?"),
("About RustDesk", "A RustDesk névjegye"),
@@ -643,7 +643,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("one-way-file-transfer-tip", "Az egyirányú fájlátvitel engedélyezve van a vezérelt oldalon."),
("Authentication Required", "Hitelesítés szükséges"),
("Authenticate", "Hitelesítés"),
("web_id_input_tip", "Azonos kiszolgálón lévő azonosítót adhat meg, a közvetlen IP elérés nem támogatott a webkliensben.\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (<id>@<kiszolgáló_cím>?key=<key_value>), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a <id>@public betűt. A kulcsra nincs szükség a nyilvános kiszolgálók esetében."),
("web_id_input_tip", "Azonos kiszolgálón lévő azonosítót adhat meg, a közvetlen IP elérés nem támogatott a webkliensben.\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (<id>@<kiszolgáló_cím>?key=<key_value>), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a \"<id>@public\" betűt. A kulcsra nincs szükség a nyilvános kiszolgálók esetében."),
("Download", "Letöltés"),
("Upload folder", "Mappa feltöltése"),
("Upload files", "Fájlok feltöltése"),
@@ -682,9 +682,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("Downloading {}", "{} letöltése"),
("{} Update", "{} frissítés"),
("{}-to-update-tip", "A(z) {} bezárása és az új verzió telepítése."),
("download-new-version-failed-tip", "Ha a letöltés sikertelen, akkor vagy újrapróbálkozhat, vagy a Letöltés gombra kattintva letöltheti a kiadási oldalról, és manuálisan frissíthet."),
("download-new-version-failed-tip", "Ha a letöltés sikertelen, akkor vagy újrapróbálkozhat, vagy a \"Letöltés\" gombra kattintva letöltheti a kiadási oldalról, és manuálisan frissíthet."),
("Auto update", "Automatikus frissítés"),
("update-failed-check-msi-tip", "A telepítési módszer felismerése nem sikerült. Kattintson a Letöltés gombra, hogy letöltse a kiadási oldalról, és manuálisan frissítse."),
("update-failed-check-msi-tip", "A telepítési módszer felismerése nem sikerült. Kattintson a \"Letöltés\" gombra, hogy letöltse a kiadási oldalról, és manuálisan frissítse."),
("websocket_tip", "WebSocket használatakor csak a relé-kapcsolatok támogatottak."),
("Use WebSocket", "WebSocket használata"),
("Trackpad speed", "Érintőpad sebessége"),
@@ -730,14 +730,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("input note here", "Megjegyzés beírása"),
("note-at-conn-end-tip", "Kérjen megjegyzést a kapcsolat végén"),
("Show terminal extra keys", "További terminálgombok megjelenítése"),
("Relative mouse mode", "Relatív egérmód"),
("rel-mouse-not-supported-peer-tip", "A kapcsolódott partner nem támogatja a relatív egérmódot."),
("rel-mouse-not-ready-tip", "A relatív egérmód még nem elérhető. Próbálja meg újra."),
("rel-mouse-lock-failed-tip", "Nem sikerült zárolni a kurzort. A relatív egérmód le lett tiltva."),
("Relative mouse mode", "Relatív egér mód"),
("rel-mouse-not-supported-peer-tip", "A kapcsolódott partner nem támogatja a relatív egér módot."),
("rel-mouse-not-ready-tip", "A relatív egér mód még nem elérhető. Próbálja meg újra."),
("rel-mouse-lock-failed-tip", "Nem sikerült zárolni a kurzort. A relatív egér mód le lett tiltva."),
("rel-mouse-exit-{}-tip", "A kilépéshez nyomja meg a(z) {} gombot."),
("rel-mouse-permission-lost-tip", "A billentyűzet-hozzáférés vissza lett vonva. A relatív egérmód le lett tilva."),
("rel-mouse-permission-lost-tip", "A billentyűzet-hozzáférés vissza lett vonva. A relatív egér mód le lett tilva."),
("Changelog", "Változáslista"),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("keep-awake-during-outgoing-sessions-label", "Képernyő aktív állapotban tartása a kimenő munkamenetek során"),
("keep-awake-during-incoming-sessions-label", "Képernyő aktív állapotban tartása a bejövő munkamenetek során"),
].iter().cloned().collect();
}

View File

@@ -737,7 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("rel-mouse-exit-{}-tip", "Premi {} per uscire."),
("rel-mouse-permission-lost-tip", "È stata revocato l'accesso alla tastiera. La modalità mouse relativa è stata disabilitata."),
("Changelog", "Novità programma"),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("keep-awake-during-outgoing-sessions-label", "Mantieni lo schermo attivo durante le sessioni in uscita"),
("keep-awake-during-incoming-sessions-label", "Mantieni lo schermo attivo durante le sessioni in ingresso"),
].iter().cloned().collect();
}

View File

@@ -737,7 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("rel-mouse-exit-{}-tip", "종료하려면 {}을(를) 누르세요."),
("rel-mouse-permission-lost-tip", "키보드 권한이 취소되었습니다. 상대 마우스 모드가 비활성화되었습니다."),
("Changelog", "변경 기록"),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("keep-awake-during-outgoing-sessions-label", "발신 세션 중 화면 켜짐 유지"),
("keep-awake-during-incoming-sessions-label", "수신 세션 중 화면 켜짐 유지"),
].iter().cloned().collect();
}

View File

@@ -737,7 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("rel-mouse-exit-{}-tip", "Druk op {} om af te sluiten."),
("rel-mouse-permission-lost-tip", "De toetsenbordcontrole is uitgeschakeld. De relatieve muismodus is uitgeschakeld."),
("Changelog", "Wijzigingenlogboek"),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("keep-awake-during-outgoing-sessions-label", "Houd het scherm open tijdens de uitgaande sessies."),
("keep-awake-during-incoming-sessions-label", "Houd het scherm open tijdens de inkomende sessies."),
].iter().cloned().collect();
}

View File

@@ -737,7 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("rel-mouse-exit-{}-tip", "Aby wyłączyć tryb przechwytywania myszy, naciśnij {}"),
("rel-mouse-permission-lost-tip", "Utracono uprawnienia do trybu przechwytywania myszy"),
("Changelog", "Dziennik zmian"),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("keep-awake-during-outgoing-sessions-label", "Utrzymuj urządzenie w stanie aktywnym podczas sesji wychodzących"),
("keep-awake-during-incoming-sessions-label", "Utrzymuj urządzenie w stanie aktywnym podczas sesji przychodzących"),
].iter().cloned().collect();
}

View File

@@ -672,72 +672,72 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
("remote-printing-disallowed-text-tip", "As configurações do dispositivo controlado não permitem impressão remota."),
("save-settings-tip", "Salvar configurações"),
("dont-show-again-tip", "Não mostrar novamente"),
("Take screenshot", ""),
("Taking screenshot", ""),
("Take screenshot", "Capturar de tela"),
("Taking screenshot", "Capturando tela"),
("screenshot-merged-screen-not-supported-tip", ""),
("screenshot-action-tip", ""),
("Save as", ""),
("Copy to clipboard", ""),
("Enable remote printer", ""),
("Save as", "Salvar como"),
("Copy to clipboard", "Copiar para área de transferência"),
("Enable remote printer", "Habilitar impressora remota"),
("Downloading {}", ""),
("{} Update", ""),
("{}-to-update-tip", ""),
("download-new-version-failed-tip", ""),
("Auto update", ""),
("update-failed-check-msi-tip", ""),
("websocket_tip", ""),
("Use WebSocket", ""),
("Trackpad speed", ""),
("download-new-version-failed-tip", "Falha no download. Você pode tentar novamente ou clicar no botão \"Download\" para baixar da página releases e atualizar manualmente."),
("Auto update", "Atualização automática"),
("update-failed-check-msi-tip", "Falha na verificação do método de instalação. Clique no botão \"Download\" para baixar da página releases e atualizar manualmente."),
("websocket_tip", "Usando WebSocket, apenas conexões via relay são suportadas."),
("Use WebSocket", "Usar WebSocket"),
("Trackpad speed", "Velocidade do trackpad"),
("Default trackpad speed", ""),
("Numeric one-time password", ""),
("Enable IPv6 P2P connection", ""),
("Enable UDP hole punching", ""),
("Numeric one-time password", "Senha numérica de uso único"),
("Enable IPv6 P2P connection", "Habilitar conexão IPv6 P2P"),
("Enable UDP hole punching", "Habilitar UDP hole punching"),
("View camera", "Visualizar câmera"),
("Enable camera", "Ativar câmera"),
("No cameras", "Sem câmeras"),
("view_camera_unsupported_tip", "O dispositivo remoto não suporta visualização da câmera."),
("Terminal", ""),
("Enable terminal", ""),
("New tab", ""),
("Keep terminal sessions on disconnect", ""),
("Terminal (Run as administrator)", ""),
("terminal-admin-login-tip", ""),
("Failed to get user token.", ""),
("Incorrect username or password.", ""),
("The user is not an administrator.", ""),
("Failed to check if the user is an administrator.", ""),
("Supported only in the installed version.", ""),
("elevation_username_tip", ""),
("Preparing for installation ...", ""),
("Show my cursor", ""),
("Terminal", "Terminal"),
("Enable terminal", "Habilitar Terminal"),
("New tab", "Nova aba"),
("Keep terminal sessions on disconnect", "Manter sessões de terminal ao desconectar"),
("Terminal (Run as administrator)", "Terminal (Executar como administrador)"),
("terminal-admin-login-tip", "Insira o nome do usuário e senha de administrador do dispositivo controlado."),
("Failed to get user token.", "Falha ao obter token do usuário."),
("Incorrect username or password.", "Usuário ou senha incorretos"),
("The user is not an administrator.", "O usuário não é administrador"),
("Failed to check if the user is an administrator.", "Falha ao verificar se o usuário é administrador"),
("Supported only in the installed version.", "Funciona somente na versão instalada"),
("elevation_username_tip", "Insira o nome do usuário ou domínio\\usuário"),
("Preparing for installation ...", "Preparando para instalação ..."),
("Show my cursor", "Mostrar meu cursor"),
("Scale custom", "Escala personalizada"),
("Custom scale slider", "Controle deslizante de escala personalizada"),
("Decrease", "Diminuir"),
("Increase", "Aumentar"),
("Show virtual mouse", ""),
("Virtual mouse size", ""),
("Small", ""),
("Large", ""),
("Show virtual mouse", "Mostrar mouse virtual"),
("Virtual mouse size", "Tamanho do mouse virtual"),
("Small", "Pequeno"),
("Large", "Grande"),
("Show virtual joystick", ""),
("Edit note", ""),
("Alias", ""),
("ScrollEdge", ""),
("Edit note", "Editar nota"),
("Alias", "Apelido"),
("ScrollEdge", "Rolagem nas bordas"),
("Allow insecure TLS fallback", ""),
("allow-insecure-tls-fallback-tip", ""),
("Disable UDP", ""),
("disable-udp-tip", ""),
("server-oss-not-support-tip", ""),
("input note here", ""),
("note-at-conn-end-tip", ""),
("Show terminal extra keys", ""),
("Relative mouse mode", ""),
("rel-mouse-not-supported-peer-tip", ""),
("rel-mouse-not-ready-tip", ""),
("rel-mouse-lock-failed-tip", ""),
("rel-mouse-exit-{}-tip", ""),
("rel-mouse-permission-lost-tip", ""),
("Changelog", ""),
("keep-awake-during-outgoing-sessions-label", ""),
("keep-awake-during-incoming-sessions-label", ""),
("allow-insecure-tls-fallback-tip", "Por padrão, o RustDesk verifica o certificado do servidor para protocolos que usam TLS.\nCom esta opção habilitada, o RustDesk ignorará a verificação e prosseguirá em caso de falha."),
("Disable UDP", "Desabilitar UDP"),
("disable-udp-tip", "Controla se deve usar somente TCP.\nCom esta opção habilitada, o RustDesk não usará mais UDP 21116, TCP 21116 será usado no lugar."),
("server-oss-not-support-tip", "NOTA: O servidor RustDesk OSS não inclui este recurso."),
("input note here", "Insira uma nota aqui"),
("note-at-conn-end-tip", "Solicitar nota ao final da conexão"),
("Show terminal extra keys", "Mostrar teclas extras do terminal"),
("Relative mouse mode", "Modo de Mouse Relativo"),
("rel-mouse-not-supported-peer-tip", "O Modo de Mouse Relativo não é suportado pelo parceiro conectado."),
("rel-mouse-not-ready-tip", "O Modo de Mouse Relativo ainda não está pronto. Por favor, tente novamente."),
("rel-mouse-lock-failed-tip", "Falha ao bloquear o cursor. O Modo de Mouse Relativo foi desabilitado."),
("rel-mouse-exit-{}-tip", "Pressione {} para sair."),
("rel-mouse-permission-lost-tip", "Permissão de teclado revogada. O Modo Mouse Relativo foi desabilitado."),
("Changelog", "Registro de alterações"),
("keep-awake-during-outgoing-sessions-label", "Manter tela ativa durante sessões de saída"),
("keep-awake-during-incoming-sessions-label", "Manter tela ativa durante sessões de entrada"),
].iter().cloned().collect();
}

View File

@@ -4,6 +4,13 @@
#include <Security/Authorization.h>
#include <Security/AuthorizationTags.h>
#include <CoreGraphics/CoreGraphics.h>
#include <vector>
#include <map>
#include <set>
#include <mutex>
#include <string>
extern "C" bool CanUseNewApiForScreenCaptureCheck() {
#ifdef NO_InputMonitoringAuthStatus
return false;
@@ -292,3 +299,624 @@ extern "C" bool MacSetMode(CGDirectDisplayID display, uint32_t width, uint32_t h
CFRelease(allModes);
return ret;
}
static CFMachPortRef g_eventTap = NULL;
static CFRunLoopSourceRef g_runLoopSource = NULL;
static std::mutex g_privacyModeMutex;
static bool g_privacyModeActive = false;
// Flag to request asynchronous shutdown of privacy mode.
// This is set by DisplayReconfigurationCallback when an error occurs, instead of calling
// TurnOffPrivacyModeInternal() directly from within the callback. This avoids potential
// issues with unregistering a callback from within itself, which is not explicitly
// guaranteed to be safe by Apple documentation.
static bool g_privacyModeShutdownRequested = false;
// Timestamp of the last display reconfiguration event (in milliseconds).
// Used for debouncing rapid successive changes (e.g., multiple resolution changes).
static uint64_t g_lastReconfigTimestamp = 0;
// Flag indicating whether a delayed blackout reapplication is already scheduled.
// Prevents multiple concurrent delayed tasks from being created.
static bool g_blackoutReapplicationScheduled = false;
// Use CFStringRef (UUID) as key instead of CGDirectDisplayID for stability across reconnections
// CGDirectDisplayID can change when displays are reconnected, but UUID remains stable
static std::map<std::string, std::vector<CGGammaValue>> g_originalGammas;
// The event source user data value used by enigo library for injected events.
// This allows us to distinguish remote input (which should be allowed) from local physical input.
// See: libs/enigo/src/macos/macos_impl.rs - ENIGO_INPUT_EXTRA_VALUE
static const int64_t ENIGO_INPUT_EXTRA_VALUE = 100;
// Duration in milliseconds to monitor and enforce blackout after display reconfiguration.
// macOS may restore default gamma (via ColorSync) at unpredictable times after display changes,
// so we need to actively monitor and reapply blackout during this period.
static const int64_t DISPLAY_RECONFIG_MONITOR_DURATION_MS = 5000;
// Interval in milliseconds between gamma checks during the monitoring period.
static const int64_t GAMMA_CHECK_INTERVAL_MS = 200;
// Helper function to get UUID string from DisplayID
static std::string GetDisplayUUID(CGDirectDisplayID displayId) {
CFUUIDRef uuid = CGDisplayCreateUUIDFromDisplayID(displayId);
if (uuid == NULL) {
return "";
}
CFStringRef uuidStr = CFUUIDCreateString(kCFAllocatorDefault, uuid);
CFRelease(uuid);
if (uuidStr == NULL) {
return "";
}
char buffer[128];
if (CFStringGetCString(uuidStr, buffer, sizeof(buffer), kCFStringEncodingUTF8)) {
CFRelease(uuidStr);
return std::string(buffer);
}
CFRelease(uuidStr);
return "";
}
// Helper function to get display name from DisplayID
static std::string GetDisplayName(CGDirectDisplayID displayId) {
// Note: NSScreen.localizedName is only available on macOS 10.15+
// Since we target 10.14, we avoid using @available checks that would
// require __isPlatformVersionAtLeast runtime function.
// For now, we just return "Unknown" for all displays.
// This is only used for logging purposes, so it doesn't affect functionality.
return "Unknown";
}
// Helper function to find DisplayID by UUID from current online displays
static CGDirectDisplayID FindDisplayIdByUUID(const std::string& targetUuid) {
uint32_t count = 0;
CGGetOnlineDisplayList(0, NULL, &count);
if (count == 0) return kCGNullDirectDisplay;
std::vector<CGDirectDisplayID> displays(count);
CGGetOnlineDisplayList(count, displays.data(), &count);
for (uint32_t i = 0; i < count; i++) {
std::string uuid = GetDisplayUUID(displays[i]);
if (uuid == targetUuid) {
return displays[i];
}
}
return kCGNullDirectDisplay;
}
// Helper function to restore gamma values for all displays in g_originalGammas.
// Returns true if all displays were restored successfully, false if any failed.
// Note: This function does NOT clear g_originalGammas - caller should do that if needed.
static bool RestoreAllGammas() {
bool allSuccess = true;
for (auto const& [uuid, gamma] : g_originalGammas) {
CGDirectDisplayID d = FindDisplayIdByUUID(uuid);
if (d == kCGNullDirectDisplay) {
NSLog(@"Display with UUID %s no longer online, skipping gamma restore", uuid.c_str());
continue;
}
uint32_t sampleCount = gamma.size() / 3;
if (sampleCount > 0) {
const CGGammaValue* red = gamma.data();
const CGGammaValue* green = red + sampleCount;
const CGGammaValue* blue = green + sampleCount;
CGError error = CGSetDisplayTransferByTable(d, sampleCount, red, green, blue);
if (error != kCGErrorSuccess) {
std::string displayName = GetDisplayName(d);
NSLog(@"Failed to restore gamma for display (Name: %s, ID: %u, UUID: %s, error: %d)",
displayName.c_str(), (unsigned)d, uuid.c_str(), error);
allSuccess = false;
}
}
}
return allSuccess;
}
// Helper function to apply blackout to a single display
static bool ApplyBlackoutToDisplay(CGDirectDisplayID display) {
uint32_t capacity = CGDisplayGammaTableCapacity(display);
if (capacity > 0) {
std::vector<CGGammaValue> zeros(capacity, 0.0f);
CGError error = CGSetDisplayTransferByTable(display, capacity, zeros.data(), zeros.data(), zeros.data());
if (error != kCGErrorSuccess) {
NSLog(@"ApplyBlackoutToDisplay: Failed to set gamma for display %u (error %d)", (unsigned)display, error);
return false;
}
return true;
}
NSLog(@"ApplyBlackoutToDisplay: Display %u has zero gamma table capacity, blackout not supported", (unsigned)display);
return false;
}
// Forward declaration - defined later in the file
// Must be called while holding g_privacyModeMutex
static bool TurnOffPrivacyModeInternal();
// Helper function to schedule asynchronous shutdown of privacy mode.
// This is called from DisplayReconfigurationCallback when an error occurs,
// instead of calling TurnOffPrivacyModeInternal() directly. This avoids
// potential issues with unregistering a callback from within itself.
// Note: This function should be called while holding g_privacyModeMutex.
static void ScheduleAsyncPrivacyModeShutdown(const char* reason) {
if (g_privacyModeShutdownRequested) {
// Already requested, no need to schedule again
return;
}
g_privacyModeShutdownRequested = true;
NSLog(@"Privacy mode shutdown requested: %s", reason);
// Schedule the actual shutdown on the main queue asynchronously
// This ensures we're outside the callback when we unregister it
dispatch_async(dispatch_get_main_queue(), ^{
std::lock_guard<std::mutex> lock(g_privacyModeMutex);
if (g_privacyModeShutdownRequested && g_privacyModeActive) {
NSLog(@"Executing deferred privacy mode shutdown");
TurnOffPrivacyModeInternal();
}
g_privacyModeShutdownRequested = false;
});
}
// Helper function to apply blackout to all online displays.
// Must be called while holding g_privacyModeMutex.
static void ApplyBlackoutToAllDisplays() {
uint32_t onlineCount = 0;
CGGetOnlineDisplayList(0, NULL, &onlineCount);
std::vector<CGDirectDisplayID> onlineDisplays(onlineCount);
CGGetOnlineDisplayList(onlineCount, onlineDisplays.data(), &onlineCount);
for (uint32_t i = 0; i < onlineCount; i++) {
ApplyBlackoutToDisplay(onlineDisplays[i]);
}
}
// Helper function to get current timestamp in milliseconds
static uint64_t GetCurrentTimestampMs() {
return (uint64_t)(CFAbsoluteTimeGetCurrent() * 1000.0);
}
// Helper function to check if a display's gamma is currently blacked out (all zeros).
// Returns true if gamma appears to be blacked out, false otherwise.
static bool IsDisplayBlackedOut(CGDirectDisplayID display) {
uint32_t capacity = CGDisplayGammaTableCapacity(display);
if (capacity == 0) {
return true; // Can't check, assume it's fine
}
std::vector<CGGammaValue> red(capacity), green(capacity), blue(capacity);
uint32_t sampleCount = 0;
if (CGGetDisplayTransferByTable(display, capacity, red.data(), green.data(), blue.data(), &sampleCount) != kCGErrorSuccess) {
return true; // Can't read, assume it's fine
}
// Check if all values are zero (or very close to zero)
for (uint32_t i = 0; i < sampleCount; i++) {
if (red[i] > 0.01f || green[i] > 0.01f || blue[i] > 0.01f) {
return false; // Not blacked out
}
}
return true;
}
// Internal function that monitors and enforces blackout for a period after display reconfiguration.
// This function checks gamma values periodically and reapplies blackout if needed.
// Must NOT be called while holding g_privacyModeMutex (it acquires the lock internally).
static void RunBlackoutMonitor() {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(GAMMA_CHECK_INTERVAL_MS * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
std::lock_guard<std::mutex> lock(g_privacyModeMutex);
if (!g_privacyModeActive) {
g_blackoutReapplicationScheduled = false;
return;
}
uint64_t now = GetCurrentTimestampMs();
// Calculate effective end time based on the last reconfig event
uint64_t effectiveEndTime = g_lastReconfigTimestamp + DISPLAY_RECONFIG_MONITOR_DURATION_MS;
// Check all displays and reapply blackout if any has been restored
uint32_t onlineCount = 0;
CGGetOnlineDisplayList(0, NULL, &onlineCount);
std::vector<CGDirectDisplayID> onlineDisplays(onlineCount);
CGGetOnlineDisplayList(onlineCount, onlineDisplays.data(), &onlineCount);
bool needsReapply = false;
for (uint32_t i = 0; i < onlineCount; i++) {
if (!IsDisplayBlackedOut(onlineDisplays[i])) {
needsReapply = true;
break;
}
}
if (needsReapply) {
NSLog(@"Gamma was restored by system, reapplying blackout");
ApplyBlackoutToAllDisplays();
}
// Continue monitoring if we haven't reached the end time
if (now < effectiveEndTime) {
RunBlackoutMonitor();
} else {
NSLog(@"Blackout monitoring period ended");
g_blackoutReapplicationScheduled = false;
}
});
}
// Helper function to start monitoring and enforcing blackout after display reconfiguration.
// This is used after display reconfiguration events because macOS may restore
// default gamma (via ColorSync) at unpredictable times after display changes.
// Note: This function should be called while holding g_privacyModeMutex.
static void ScheduleDelayedBlackoutReapplication(const char* reason) {
// Update timestamp to current time
g_lastReconfigTimestamp = GetCurrentTimestampMs();
NSLog(@"Starting blackout monitor: %s", reason);
// Only schedule if not already scheduled
if (!g_blackoutReapplicationScheduled) {
g_blackoutReapplicationScheduled = true;
RunBlackoutMonitor();
}
// If already scheduled, the running monitor will see the updated timestamp
// and extend its monitoring period
}
// Display reconfiguration callback to handle display connect/disconnect events
//
// IMPORTANT: When errors occur in this callback, we use ScheduleAsyncPrivacyModeShutdown()
// instead of calling TurnOffPrivacyModeInternal() directly. This is because:
// 1. TurnOffPrivacyModeInternal() calls CGDisplayRemoveReconfigurationCallback to unregister
// this callback, and unregistering a callback from within itself is not explicitly
// guaranteed to be safe by Apple documentation.
// 2. Using async dispatch ensures we're completely outside the callback context when
// performing the cleanup, avoiding any potential undefined behavior.
static void DisplayReconfigurationCallback(CGDirectDisplayID display, CGDisplayChangeSummaryFlags flags, void *userInfo) {
(void)userInfo;
// Note: We need to handle the callback carefully because:
// 1. macOS may call this callback multiple times during display reconfiguration
// 2. The system may restore ColorSync settings after our gamma change
// 3. We should not hold the lock for too long in the callback
// Skip begin configuration flag - wait for the actual change
if (flags & kCGDisplayBeginConfigurationFlag) {
return;
}
std::lock_guard<std::mutex> lock(g_privacyModeMutex);
if (!g_privacyModeActive) {
return;
}
if (flags & kCGDisplayAddFlag) {
// A display was added - apply blackout to it
NSLog(@"Display %u added during privacy mode, applying blackout", (unsigned)display);
std::string uuid = GetDisplayUUID(display);
if (uuid.empty()) {
NSLog(@"Failed to get UUID for newly added display %u, exiting privacy mode", (unsigned)display);
ScheduleAsyncPrivacyModeShutdown("Failed to get UUID for newly added display");
return;
}
// Save original gamma if not already saved for this UUID
if (g_originalGammas.find(uuid) == g_originalGammas.end()) {
uint32_t capacity = CGDisplayGammaTableCapacity(display);
if (capacity > 0) {
std::vector<CGGammaValue> red(capacity), green(capacity), blue(capacity);
uint32_t sampleCount = 0;
if (CGGetDisplayTransferByTable(display, capacity, red.data(), green.data(), blue.data(), &sampleCount) == kCGErrorSuccess) {
std::vector<CGGammaValue> all;
all.insert(all.end(), red.begin(), red.begin() + sampleCount);
all.insert(all.end(), green.begin(), green.begin() + sampleCount);
all.insert(all.end(), blue.begin(), blue.begin() + sampleCount);
g_originalGammas[uuid] = all;
} else {
NSLog(@"DisplayReconfigurationCallback: Failed to get gamma table for display %u (UUID: %s), exiting privacy mode", (unsigned)display, uuid.c_str());
ScheduleAsyncPrivacyModeShutdown("Failed to get gamma table for newly added display");
return;
}
} else {
NSLog(@"DisplayReconfigurationCallback: Display %u (UUID: %s) has zero gamma table capacity, exiting privacy mode", (unsigned)display, uuid.c_str());
ScheduleAsyncPrivacyModeShutdown("Newly added display has zero gamma table capacity");
return;
}
}
// Apply blackout to the new display immediately
if (!ApplyBlackoutToDisplay(display)) {
NSLog(@"DisplayReconfigurationCallback: Failed to blackout display %u (UUID: %s), exiting privacy mode", (unsigned)display, uuid.c_str());
ScheduleAsyncPrivacyModeShutdown("Failed to blackout newly added display");
return;
}
// Schedule a delayed re-application to handle ColorSync restoration
// macOS may restore default gamma for ALL displays after a new display is added,
// so we need to reapply blackout to all online displays, not just the new one
ScheduleDelayedBlackoutReapplication("after new display added");
} else if (flags & kCGDisplayRemoveFlag) {
// A display was removed - update our mapping and reapply blackout to remaining displays
NSLog(@"Display %u removed during privacy mode", (unsigned)display);
std::string uuid = GetDisplayUUID(display);
(void)uuid; // UUID retrieved for potential future use or logging
// When a display is removed, macOS may reconfigure other displays and restore their gamma.
// Schedule a delayed re-application of blackout to all remaining online displays.
ScheduleDelayedBlackoutReapplication("after display removal");
} else if (flags & kCGDisplaySetModeFlag) {
// Display mode changed (resolution change, ColorSync/Night Shift interference, etc.)
// macOS resets gamma to default when display mode changes, so we need to reapply blackout.
// Schedule a delayed re-application because ColorSync restoration happens asynchronously.
NSLog(@"Display %u mode changed during privacy mode, reapplying blackout", (unsigned)display);
ScheduleDelayedBlackoutReapplication("after display mode change");
}
}
CGEventRef MyEventTapCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
(void)proxy;
(void)refcon;
// Handle EventTap being disabled by system timeout
if (type == kCGEventTapDisabledByTimeout) {
NSLog(@"EventTap was disabled by timeout, re-enabling");
if (g_eventTap) {
CGEventTapEnable(g_eventTap, true);
}
return event;
}
// Handle EventTap being disabled by user input
if (type == kCGEventTapDisabledByUserInput) {
NSLog(@"EventTap was disabled by user input, re-enabling");
if (g_eventTap) {
CGEventTapEnable(g_eventTap, true);
}
return event;
}
// Allow events explicitly injected by enigo (remote input), identified via custom user data.
int64_t userData = CGEventGetIntegerValueField(event, kCGEventSourceUserData);
if (userData == ENIGO_INPUT_EXTRA_VALUE) {
return event;
}
// Block local physical HID input.
if (CGEventGetIntegerValueField(event, kCGEventSourceStateID) == kCGEventSourceStateHIDSystemState) {
return NULL;
}
return event;
}
// Helper function to set up EventTap on the main thread
// Returns true if EventTap was successfully created and enabled
static bool SetupEventTapOnMainThread() {
__block bool success = false;
void (^setupBlock)(void) = ^{
if (g_eventTap) {
// Already set up
success = true;
return;
}
// Note: kCGEventTapDisabledByTimeout and kCGEventTapDisabledByUserInput are special
// notification types (0xFFFFFFFE and 0xFFFFFFFF) that are delivered via the callback's
// type parameter, not through the event mask. They should NOT be included in eventMask
// as bit-shifting by these values causes undefined behavior.
CGEventMask eventMask = (1 << kCGEventKeyDown) | (1 << kCGEventKeyUp) |
(1 << kCGEventLeftMouseDown) | (1 << kCGEventLeftMouseUp) |
(1 << kCGEventRightMouseDown) | (1 << kCGEventRightMouseUp) |
(1 << kCGEventOtherMouseDown) | (1 << kCGEventOtherMouseUp) |
(1 << kCGEventLeftMouseDragged) | (1 << kCGEventRightMouseDragged) |
(1 << kCGEventOtherMouseDragged) |
(1 << kCGEventMouseMoved) | (1 << kCGEventScrollWheel);
g_eventTap = CGEventTapCreate(kCGHIDEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault,
eventMask, MyEventTapCallback, NULL);
if (g_eventTap) {
g_runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, g_eventTap, 0);
CFRunLoopAddSource(CFRunLoopGetMain(), g_runLoopSource, kCFRunLoopCommonModes);
CGEventTapEnable(g_eventTap, true);
success = true;
} else {
NSLog(@"MacSetPrivacyMode: Failed to create CGEventTap; input blocking not enabled.");
success = false;
}
};
// Execute on main thread to ensure CFRunLoop operations are safe.
// Use dispatch_sync if not on main thread, otherwise execute directly to avoid deadlock.
//
// IMPORTANT: Potential deadlock consideration:
// Using dispatch_sync while holding g_privacyModeMutex could deadlock if the main thread
// tries to acquire g_privacyModeMutex. Currently this is safe because:
// 1. MacSetPrivacyMode (which holds the mutex) is only called from background threads
// 2. The main thread never directly calls MacSetPrivacyMode
// If this assumption changes in the future, consider releasing the mutex before dispatch_sync
// or restructuring the locking strategy.
if ([NSThread isMainThread]) {
setupBlock();
} else {
dispatch_sync(dispatch_get_main_queue(), setupBlock);
}
return success;
}
// Helper function to tear down EventTap on the main thread
static void TeardownEventTapOnMainThread() {
void (^teardownBlock)(void) = ^{
if (g_eventTap) {
CGEventTapEnable(g_eventTap, false);
CFRunLoopRemoveSource(CFRunLoopGetMain(), g_runLoopSource, kCFRunLoopCommonModes);
CFRelease(g_runLoopSource);
CFRelease(g_eventTap);
g_eventTap = NULL;
g_runLoopSource = NULL;
}
};
// Execute on main thread to ensure CFRunLoop operations are safe.
//
// NOTE: We use dispatch_sync here instead of dispatch_async because:
// 1. TurnOffPrivacyModeInternal() expects EventTap to be fully torn down before
// proceeding with gamma restoration - using async would cause race conditions.
// 2. The caller (MacSetPrivacyMode) needs deterministic cleanup order.
//
// IMPORTANT: Potential deadlock consideration (same as SetupEventTapOnMainThread):
// Using dispatch_sync while holding g_privacyModeMutex could deadlock if the main thread
// tries to acquire g_privacyModeMutex. Currently this is safe because:
// 1. MacSetPrivacyMode (which holds the mutex) is only called from background threads
// 2. The main thread never directly calls MacSetPrivacyMode
// If this assumption changes in the future, consider releasing the mutex before dispatch_sync
// or restructuring the locking strategy.
if ([NSThread isMainThread]) {
teardownBlock();
} else {
dispatch_sync(dispatch_get_main_queue(), teardownBlock);
}
}
// Internal function to turn off privacy mode without acquiring the mutex
// Must be called while holding g_privacyModeMutex
static bool TurnOffPrivacyModeInternal() {
if (!g_privacyModeActive) {
return true;
}
// 1. Unregister display reconfiguration callback
CGDisplayRemoveReconfigurationCallback(DisplayReconfigurationCallback, NULL);
// 2. Input - restore (tear down EventTap on main thread)
TeardownEventTapOnMainThread();
// 3. Gamma - restore using UUID to find current DisplayID
bool restoreSuccess = RestoreAllGammas();
// 4. Fallback: Always call CGDisplayRestoreColorSyncSettings as a safety net
// This ensures displays return to normal even if our restoration failed or
// if the system (ColorSync/Night Shift) modified gamma during privacy mode
CGDisplayRestoreColorSyncSettings();
// Clean up
g_originalGammas.clear();
g_privacyModeActive = false;
g_privacyModeShutdownRequested = false;
g_lastReconfigTimestamp = 0;
g_blackoutReapplicationScheduled = false;
return restoreSuccess;
}
extern "C" bool MacSetPrivacyMode(bool on) {
std::lock_guard<std::mutex> lock(g_privacyModeMutex);
if (on) {
// Already in privacy mode
if (g_privacyModeActive) {
return true;
}
// 1. Input Blocking - set up EventTap on main thread
if (!SetupEventTapOnMainThread()) {
return false;
}
// 2. Register display reconfiguration callback to handle hot-plug events
CGDisplayRegisterReconfigurationCallback(DisplayReconfigurationCallback, NULL);
// 3. Gamma Blackout
uint32_t count = 0;
CGGetOnlineDisplayList(0, NULL, &count);
std::vector<CGDirectDisplayID> displays(count);
CGGetOnlineDisplayList(count, displays.data(), &count);
uint32_t blackoutSuccessCount = 0;
uint32_t blackoutAttemptCount = 0;
for (uint32_t i = 0; i < count; i++) {
CGDirectDisplayID d = displays[i];
std::string uuid = GetDisplayUUID(d);
if (uuid.empty()) {
NSLog(@"MacSetPrivacyMode: Failed to get UUID for display %u, privacy mode requires all displays", (unsigned)d);
// Privacy mode requires ALL connected displays to be successfully blacked out
// to ensure user privacy. If we can't identify a display (no UUID),
// we can't safely manage its state or restore it later.
// Therefore, we must abort the entire operation and clean up any resources
// already allocated (like event taps and reconfiguration callbacks).
CGDisplayRemoveReconfigurationCallback(DisplayReconfigurationCallback, NULL);
TeardownEventTapOnMainThread();
// Restore gamma for displays that were already blacked out before this failure
if (!RestoreAllGammas()) {
// If any display failed to restore, use system reset as fallback
CGDisplayRestoreColorSyncSettings();
}
g_originalGammas.clear();
return false;
}
// Save original gamma using UUID as key (stable across reconnections)
if (g_originalGammas.find(uuid) == g_originalGammas.end()) {
uint32_t capacity = CGDisplayGammaTableCapacity(d);
if (capacity > 0) {
std::vector<CGGammaValue> red(capacity), green(capacity), blue(capacity);
uint32_t sampleCount = 0;
if (CGGetDisplayTransferByTable(d, capacity, red.data(), green.data(), blue.data(), &sampleCount) == kCGErrorSuccess) {
std::vector<CGGammaValue> all;
all.insert(all.end(), red.begin(), red.begin() + sampleCount);
all.insert(all.end(), green.begin(), green.begin() + sampleCount);
all.insert(all.end(), blue.begin(), blue.begin() + sampleCount);
g_originalGammas[uuid] = all;
} else {
NSLog(@"MacSetPrivacyMode: Failed to get gamma table for display %u (UUID: %s)", (unsigned)d, uuid.c_str());
}
} else {
NSLog(@"MacSetPrivacyMode: Display %u (UUID: %s) has zero gamma table capacity, not supported", (unsigned)d, uuid.c_str());
}
}
// Set to black only if we have saved original gamma for this display
if (g_originalGammas.find(uuid) != g_originalGammas.end()) {
uint32_t capacity = CGDisplayGammaTableCapacity(d);
if (capacity > 0) {
std::vector<CGGammaValue> zeros(capacity, 0.0f);
blackoutAttemptCount++;
CGError error = CGSetDisplayTransferByTable(d, capacity, zeros.data(), zeros.data(), zeros.data());
if (error != kCGErrorSuccess) {
std::string displayName = GetDisplayName(d);
NSLog(@"MacSetPrivacyMode: Failed to blackout display (Name: %s, ID: %u, UUID: %s, error: %d)", displayName.c_str(), (unsigned)d, uuid.c_str(), error);
} else {
blackoutSuccessCount++;
}
} else {
NSLog(@"MacSetPrivacyMode: Display %u (UUID: %s) has zero gamma table capacity for blackout", (unsigned)d, uuid.c_str());
}
}
}
// Return false if any display failed to blackout - privacy mode requires ALL displays to be blacked out
if (blackoutAttemptCount > 0 && blackoutSuccessCount < blackoutAttemptCount) {
NSLog(@"MacSetPrivacyMode: Failed to blackout all displays (%u/%u succeeded)", blackoutSuccessCount, blackoutAttemptCount);
// Clean up: unregister callback and disable event tap since we're failing
CGDisplayRemoveReconfigurationCallback(DisplayReconfigurationCallback, NULL);
TeardownEventTapOnMainThread();
// Restore gamma for displays that were successfully blacked out
if (!RestoreAllGammas()) {
// If any display failed to restore, use system reset as fallback
NSLog(@"Some displays failed to restore gamma during cleanup, using CGDisplayRestoreColorSyncSettings as fallback");
CGDisplayRestoreColorSyncSettings();
}
g_originalGammas.clear();
return false;
}
g_privacyModeActive = true;
return true;
} else {
return TurnOffPrivacyModeInternal();
}
}

View File

@@ -23,6 +23,9 @@ pub mod win_mag;
#[cfg(windows)]
pub mod win_topmost_window;
#[cfg(target_os = "macos")]
pub mod macos;
#[cfg(windows)]
mod win_virtual_display;
#[cfg(windows)]
@@ -105,7 +108,14 @@ lazy_static::lazy_static! {
}
#[cfg(not(windows))]
{
"".to_owned()
#[cfg(target_os = "macos")]
{
macos::PRIVACY_MODE_IMPL.to_owned()
}
#[cfg(not(target_os = "macos"))]
{
"".to_owned()
}
}
};
@@ -127,7 +137,13 @@ pub type PrivacyModeCreator = fn(impl_key: &str) -> Box<dyn PrivacyMode>;
lazy_static::lazy_static! {
static ref PRIVACY_MODE_CREATOR: Arc<Mutex<HashMap<&'static str, PrivacyModeCreator>>> = {
#[cfg(not(windows))]
let map: HashMap<&'static str, PrivacyModeCreator> = HashMap::new();
let mut map: HashMap<&'static str, PrivacyModeCreator> = HashMap::new();
#[cfg(target_os = "macos")]
{
map.insert(macos::PRIVACY_MODE_IMPL, |impl_key: &str| {
Box::new(macos::PrivacyModeImpl::new(impl_key))
});
}
#[cfg(windows)]
let mut map: HashMap<&'static str, PrivacyModeCreator> = HashMap::new();
#[cfg(windows)]
@@ -333,7 +349,14 @@ pub fn get_supported_privacy_mode_impl() -> Vec<(&'static str, &'static str)> {
vec_impls
}
#[cfg(not(target_os = "windows"))]
#[cfg(target_os = "macos")]
{
// No translation is intended for privacy_mode_impl_macos_tip as it is a
// placeholder for macOS specific privacy mode implementation which currently
// doesn't provide multiple modes like Windows does.
vec![(macos::PRIVACY_MODE_IMPL, "privacy_mode_impl_macos_tip")]
}
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
{
Vec::new()
}

81
src/privacy_mode/macos.rs Normal file
View File

@@ -0,0 +1,81 @@
use super::{PrivacyMode, PrivacyModeState};
use hbb_common::{anyhow::anyhow, ResultType};
extern "C" {
fn MacSetPrivacyMode(on: bool) -> bool;
}
pub const PRIVACY_MODE_IMPL: &str = "privacy_mode_impl_macos";
pub struct PrivacyModeImpl {
impl_key: String,
conn_id: i32,
}
impl PrivacyModeImpl {
pub fn new(impl_key: &str) -> Self {
Self {
impl_key: impl_key.to_owned(),
conn_id: 0,
}
}
}
impl PrivacyMode for PrivacyModeImpl {
fn is_async_privacy_mode(&self) -> bool {
false
}
fn init(&self) -> ResultType<()> {
Ok(())
}
fn clear(&mut self) {
unsafe {
MacSetPrivacyMode(false);
}
self.conn_id = 0;
}
fn turn_on_privacy(&mut self, conn_id: i32) -> ResultType<bool> {
if self.check_on_conn_id(conn_id)? {
return Ok(true);
}
let success = unsafe { MacSetPrivacyMode(true) };
if !success {
return Err(anyhow!("Failed to turn on privacy mode"));
}
self.conn_id = conn_id;
Ok(true)
}
fn turn_off_privacy(&mut self, conn_id: i32, _state: Option<PrivacyModeState>) -> ResultType<()> {
// Note: The `_state` parameter is intentionally ignored on macOS.
// On Windows, it's used to notify the connection manager about privacy mode state changes
// (see win_topmost_window.rs). macOS currently has a simpler single-mode implementation
// without the need for such cross-component state synchronization.
self.check_off_conn_id(conn_id)?;
let success = unsafe { MacSetPrivacyMode(false) };
if !success {
return Err(anyhow!("Failed to turn off privacy mode"));
}
self.conn_id = 0;
Ok(())
}
fn pre_conn_id(&self) -> i32 {
self.conn_id
}
fn get_impl_key(&self) -> &str {
&self.impl_key
}
}
impl Drop for PrivacyModeImpl {
fn drop(&mut self) {
// Use the same cleanup logic as other code paths to keep conn_id consistent
// and ensure all cleanup is centralized in one place.
self.clear();
}
}

View File

@@ -1420,7 +1420,7 @@ impl Connection {
pi.platform = "Android".into();
}
#[cfg(all(target_os = "macos", not(feature = "unix-file-copy-paste")))]
let platform_additions = serde_json::Map::new();
let mut platform_additions = serde_json::Map::new();
#[cfg(any(
target_os = "windows",
target_os = "linux",
@@ -1453,6 +1453,13 @@ impl Connection {
json!(privacy_mode::get_supported_privacy_mode_impl()),
);
}
#[cfg(target_os = "macos")]
{
platform_additions.insert(
"supported_privacy_mode_impl".into(),
json!(privacy_mode::get_supported_privacy_mode_impl()),
);
}
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
{