Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
37 changes: 37 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -297,3 +297,40 @@ Server.Installer/Properties/launchSettings.json
!/.vscode/tasks.json
/Server/appsettings.Development.json
/Server/AppData
# build/IDE
bin/
obj/
.vs/
*.user
*.suo

# artifacts
*.zip
*.7z
*.tar
*.tgz

# system
Thumbs.db
bin/
obj/
.vs/
*.user
*.suo
*.zip
*.7z
*.tgz
Thumbs.db
# .NET
bin/
obj/
.vs/

# Rider/other IDEs
.idea/

# Publish & local artifacts (don’t commit compiled builds)
publish-linux/
artifacts/
Server/wwwroot/Downloads/
AppData/
4 changes: 3 additions & 1 deletion Desktop.Shared/Enums/ButtonAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@
public enum ButtonAction
{
Down,
Up
Up,
PrivacyOn,
PrivacyOff
}
14 changes: 14 additions & 0 deletions Desktop.Shared/Messages/ButtonActionMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Remotely.Desktop.Shared.Messages;

using Remotely.Desktop.Shared.Enums;

public sealed class ButtonActionMessage
{
public ButtonAction Action { get; set; }
}
2 changes: 2 additions & 0 deletions Desktop.Win/Desktop.Win.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
<StartupObject></StartupObject>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)'=='Release'">
Expand Down
100 changes: 100 additions & 0 deletions Desktop.Win/PrivacyOverlay.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace Remotely.Desktop.Win;

/// <summary>
/// Fullscreen black overlay per monitor that is excluded from screen capture
/// (local user sees black) but is click-through and non-activating so remote
/// input still reaches underlying windows.
/// </summary>
internal static class PrivacyOverlay
{
private static readonly List<Form> _overlays = new();

[DllImport("user32.dll", SetLastError = true)]
private static extern bool SetWindowDisplayAffinity(IntPtr hWnd, uint dwAffinity);

private const uint WDA_NONE = 0;
private const uint WDA_EXCLUDEFROMCAPTURE = 0x11; // Windows 10 2004+ (19041)

// ---- Custom overlay form (click-through, non-activating, hidden from Alt+Tab) ----
private sealed class OverlayForm : Form
{
// WS_EX_TOOLWINDOW (0x00000080) - hide from Alt+Tab
// WS_EX_LAYERED (0x00080000) - layered window
// WS_EX_TRANSPARENT(0x00000020) - mouse clicks pass through
// WS_EX_NOACTIVATE (0x08000000) - never takes focus
protected override CreateParams CreateParams
{
get
{
var cp = base.CreateParams;
cp.ExStyle |= 0x00000080 | 0x00080000 | 0x00000020 | 0x08000000;
return cp;
}
}

protected override bool ShowWithoutActivation => true;
}

public static bool IsActive => _overlays.Count > 0;

public static void Enable()
{
if (IsActive) return;

foreach (var screen in Screen.AllScreens)
{
var f = new OverlayForm
{
FormBorderStyle = FormBorderStyle.None,
StartPosition = FormStartPosition.Manual,
Bounds = screen.Bounds,
BackColor = Color.Black,
TopMost = true,
ShowInTaskbar = false,
Opacity = 1
};

f.Load += (_, __) =>
{
// Exclude from capture so only the local user sees black.
_ = SetWindowDisplayAffinity(f.Handle, WDA_EXCLUDEFROMCAPTURE);
};

// Best-effort: restore affinity on close
f.FormClosed += (_, __) =>
{
try { _ = SetWindowDisplayAffinity(f.Handle, WDA_NONE); } catch { }
};

f.Show(); // non-activating
_overlays.Add(f);
}
}

public static void Disable()
{
foreach (var f in _overlays)
{
try
{
_ = SetWindowDisplayAffinity(f.Handle, WDA_NONE);
if (!f.IsDisposed) f.Close();
f.Dispose();
}
catch { /* ignore */ }
}
_overlays.Clear();
}

public static void Toggle()
{
if (IsActive) Disable();
else Enable();
}
}
67 changes: 67 additions & 0 deletions Desktop.Win/Services/AppStartup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
using Remotely.Desktop.Shared.Services;
using Remotely.Desktop.UI.Services;
using Remotely.Shared.Models;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using Remotely.Desktop.Win; // <� add this if not already present




namespace Remotely.Desktop.Win.Services;

Expand All @@ -21,6 +28,9 @@ internal class AppStartup : IAppStartup
private readonly IShutdownService _shutdownService;
private readonly IBrandingProvider _brandingProvider;
private readonly ILogger<AppStartup> _logger;
private NotifyIcon? _tray;
private ToolStripMenuItem? _privacyItem;


public AppStartup(
IAppState appState,
Expand Down Expand Up @@ -54,7 +64,64 @@ public async Task Run()
{
await _brandingProvider.Initialize();


_messageLoop.StartMessageLoop();
// ===== Tray icon + Privacy toggle =====
if (_tray is null)
{
_tray = new NotifyIcon();

// Try to load an icon from Assets; fallback to default
var iconPath = Path.Combine(AppContext.BaseDirectory, "Assets", "favicon.ico");
_tray.Icon = File.Exists(iconPath) ? new Icon(iconPath) : SystemIcons.Application;
_tray.Visible = true;
_tray.Text = "Remotely Agent";

var menu = new ContextMenuStrip();

_privacyItem = new ToolStripMenuItem("Privacy Mode (Black Screen)")
{
CheckOnClick = true
};
_privacyItem.CheckedChanged += (s, e) =>
{
if (_privacyItem.Checked)
{
PrivacyOverlay.Enable();
}
else
{
PrivacyOverlay.Disable();
}
};

var exitItem = new ToolStripMenuItem("Exit Agent");
exitItem.Click += (s, e) =>
{
try { PrivacyOverlay.Disable(); } catch { }
if (_tray is not null) { _tray.Visible = false; }
Application.Exit();
};

menu.Items.Add(_privacyItem);
menu.Items.Add(new ToolStripSeparator());
menu.Items.Add(exitItem);

_tray.ContextMenuStrip = menu;
}

// Ensure cleanup when the app is exiting (service stop / app exit)
_uiDispatcher.ApplicationExitingToken.Register(() =>
{
try { PrivacyOverlay.Disable(); } catch { }
if (_tray is not null)
{
_tray.Visible = false;
_tray.Dispose();
_tray = null;
}
});


if (_appState.Mode is AppMode.Unattended or AppMode.Attended)
{
Expand Down
Loading