Skip to content
Merged
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ it's a self-contained single file, no .NET install required (Windows 10/11 x64).
### Protocols, one tree

- **RDP** — Microsoft's official ActiveX control (`mstscax`): dynamic resolution that follows the
window/full-screen size (falls back to smart-sizing on older hosts), clipboard/drive/printer/audio
redirection, server-identity verification level, RD Gateway / jump-host and admin session.
window/full-screen size (falls back to smart-sizing on older hosts) or a **fixed resolution** of your
choice, **remote desktop scaling (DPI)** so text stays readable on 4K/HiDPI screens
(*Settings → Connection*), clipboard/drive/printer/audio redirection, server-identity verification
level, RD Gateway / jump-host and admin session.
- **SSH** — embedded **xterm.js** terminal: password and private-key auth (including
**passphrase-protected keys**), host-key verification (trust-on-first-use with fingerprint prompt),
local **port-forwarding tunnels**, copy-on-select, Ctrl+Shift+C/V, Ctrl+wheel font size.
Expand Down
3 changes: 2 additions & 1 deletion docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ current priorities.
- ✅ **Focus mode** — maximized main window hides all chrome except the tab strip;
window controls move onto the strip. Distinct from true full screen (F11).
- ✅ **Full-screen** with auto-hiding, draggable, pinnable bar + "other connections" flyout.
- ✅ **Dynamic resolution** — session = window/monitor physical pixels, DPI-safe.
- ✅ **Dynamic resolution** — session = window/monitor physical pixels, DPI-safe; optionally a
fixed resolution, plus remote desktop scaling (DPI) for readable text on 4K/HiDPI screens.
- ✅ **Credentials in Windows Credential Manager** (DPAPI); prompt at connect; "connect as".
- ✅ **Server tree** — groups, favorites (pinned), collapsible groups, one-click group
rename, search, drag-and-drop reorder, background reachability dots.
Expand Down
11 changes: 11 additions & 0 deletions src/RdpManager/AppSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ public class AppSettings
/// <summary>Domyślne ustawienia połączeń RDP.</summary>
public bool AutoReconnect { get; set; } = true;
public int ColorDepth { get; set; } = 32;

/// <summary>Rozdzielczość sesji RDP: 0×0 (domyślnie) = „dopasuj do okna", czyli sesja trzyma natywne
/// piksele panelu (obraz 1:1). Wartość stała = pulpit negocjuje ten rozmiar, a SmartSizing rozciąga go
/// do okna. Wspólne dla wszystkich serwerów RDP. Przeliczenia: <see cref="Core.RdpDisplay"/>.</summary>
public int RdpDesktopWidth { get; set; }
public int RdpDesktopHeight { get; set; }

/// <summary>Skalowanie (DPI) pulpitu zdalnego w procentach: 0 (domyślnie) = „jak ekran lokalny",
/// inaczej 100..500. Powiększa tekst i ikony W SESJI, bez zmiany rozdzielczości i bez rozmycia —
/// na ekranach 4K/HiDPI sesja 1:1 przy 100% daje interfejs nie do odczytania.</summary>
public int RdpScalePercent { get; set; }
// Domyślne przekierowania dla NOWO tworzonych serwerów RDP (istniejące zachowują swoje).
public bool DefaultRedirectClipboard { get; set; } = true;
public bool DefaultRedirectDrives { get; set; } = false; // dyski + drukarki
Expand Down
5 changes: 4 additions & 1 deletion src/RdpManager/Controllers/SessionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ internal void OpenServer(ServerInfo server, bool autoConnect = false, bool force
try { ((System.Windows.Forms.Control)rdp).CreateControl(); } catch { } // wymuś utworzenie kontrolki ActiveX

session = new Session(server, rdp, host);
session.Resizer = new RdpDynamicResolution(session, host);
session.Resizer = new RdpDynamicResolution(session, host, _owner._settings);
WireEvents(session);
// W podziale: klik w panel (RDP przejmuje fokus klawiatury) czyni go aktywnym — karta i toolbar podążają.
var focusTarget = session;
Expand Down Expand Up @@ -580,6 +580,9 @@ private void ConnectSession(Session s)
}
catch { /* starsza kontrolka bez RemoteProgram2 — łączymy jako pełny pulpit */ }

// Rozdzielczość + skala DPI z ustawień — żeby pierwsza klatka sesji była już poprawna.
s.Resizer?.ApplyPreConnect();

s.Rdp.Connect();
SetSessionStatus(s, string.Format(L("S.st.connecting"), s.Server.Host), StatusKind.Connecting);
}
Expand Down
86 changes: 86 additions & 0 deletions src/RdpManager/Core/RdpDisplay.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System;
using System.Globalization;

namespace RdpManager.Core
{
/// <summary>
/// Czyste przeliczenia rozdzielczości i skalowania (DPI) sesji RDP — bez WPF/ActiveX, więc testowalne.
/// Na kontrolce stosuje je <c>RdpDynamicResolution</c>.
///
/// Dwa niezależne pokrętła (Ustawienia → Połączenie → Domyślne RDP):
/// - ROZDZIELCZOŚĆ: 0×0 = „dopasuj do okna" (domyślne — sesja trzyma natywne piksele panelu, obraz 1:1),
/// wartość stała (np. 1600×900) = sesja negocjuje ten rozmiar, a SmartSizing rozciąga go do okna.
/// - SKALOWANIE (DPI): 0 = „jak ekran lokalny" (mnożnik DPI okna — tak robi mstsc), inaczej stałe procenty.
/// Trafia do ulDesktopScaleFactor (UpdateSessionDisplaySettings) i „DesktopScaleFactor" przed Connect,
/// więc zdalny pulpit RYSUJE interfejs większy — bez utraty ostrości (odwrotnie niż SmartSizing).
/// Na ekranie 4K/HiDPI sesja 1:1 przy 100% daje mikroskopijny tekst; to jest na to lekarstwo.
/// </summary>
public static class RdpDisplay
{
// Zakresy z dokumentacji IMsRdpClient9::UpdateSessionDisplaySettings: ulDesktopScaleFactor 100..500,
// ulDeviceScaleFactor TYLKO 100/140/180 (inna wartość => E_INVALIDARG, cała zmiana odrzucona).
// Device trzymamy na 100 („bez dodatkowego skalowania urządzenia") — DPI sesji ustala desktopScaleFactor.
public const int MinScale = 100;
public const int MaxScale = 500;
public const uint DeviceScaleFactor = 100u;

/// <summary>Czy ustawienie opisuje STAŁĄ rozdzielczość (a nie „dopasuj do okna").</summary>
public static bool IsFixed(int width, int height)
=> width >= RdpUtils.MinDim && height >= RdpUtils.MinDim;

/// <summary>
/// Sprowadza zapisaną parę do postaci używalnej przez kontrolkę: (0, 0) = „dopasuj do okna",
/// inaczej wymiary znormalizowane jak przy Display-Update (parzyste, [200..8192]).
/// Wartości poniżej minimum traktujemy jak brak wyboru — plik settings.json bywa edytowany ręcznie.
/// </summary>
public static (int Width, int Height) Normalize(int width, int height)
=> IsFixed(width, height)
? (RdpUtils.NormalizeDim(width), RdpUtils.NormalizeDim(height))
: (0, 0);

/// <summary>„1920x1080" (albo „1920×1080") → (1920, 1080); „", „Auto", śmieci → (0, 0) = dopasuj do okna.</summary>
public static (int Width, int Height) ParseResolution(string tag)
{
string s = (tag ?? "").Trim();
if (s.Length == 0) return (0, 0);

int sep = s.IndexOfAny(new[] { 'x', 'X', '×' });
if (sep <= 0 || sep == s.Length - 1) return (0, 0);

if (!int.TryParse(s.Substring(0, sep).Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var w)) return (0, 0);
if (!int.TryParse(s.Substring(sep + 1).Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var h)) return (0, 0);
return Normalize(w, h);
}

/// <summary>Odwrotność <see cref="ParseResolution"/> — Tag pozycji listy dla zapisanej wartości („" = auto).</summary>
public static string FormatResolution(int width, int height)
{
var (w, h) = Normalize(width, height);
return w == 0 ? "" : w.ToString(CultureInfo.InvariantCulture) + "x" + h.ToString(CultureInfo.InvariantCulture);
}

/// <summary>Tag pozycji listy skalowania → wartość do zapisu: „" / „Auto" / śmieci = 0 („jak ekran lokalny").</summary>
public static int ParseScale(string tag)
{
string s = (tag ?? "").Trim().TrimEnd('%').Trim();
if (s.Length == 0) return 0;
if (!int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var pct) || pct <= 0) return 0;
return Math.Clamp(pct, MinScale, MaxScale);
}

/// <summary>
/// Skala DPI wysyłana serwerowi. <paramref name="configured"/> &gt; 0 = wybór użytkownika (przycięty do
/// [100..500]); 0 = „jak ekran lokalny", czyli mnożnik DPI okna (1.5 → 150%). Zaokrąglamy do 5%, bo
/// mnożnik z <c>VisualTreeHelper.GetDpi</c> bywa niecałkowity (np. 1.4999), a serwer i tak operuje
/// na okrągłych krokach DPI.
/// </summary>
public static int EffectiveScale(int configured, double localDpiScale)
{
if (configured > 0) return Math.Clamp(configured, MinScale, MaxScale);
if (double.IsNaN(localDpiScale) || double.IsInfinity(localDpiScale) || localDpiScale <= 0) return MinScale;

int pct = (int)Math.Round(localDpiScale * 100.0 / 5.0, MidpointRounding.AwayFromZero) * 5;
return Math.Clamp(pct, MinScale, MaxScale);
}
}
}
39 changes: 39 additions & 0 deletions src/RdpManager/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,45 @@
<ui:TextBox x:Name="SetDefaultPort" Grid.Column="1" Width="90" VerticalAlignment="Center"/>
</Grid>
</Border>
<Border Style="{DynamicResource SetRow}">
<Grid>
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
<StackPanel VerticalAlignment="Center">
<TextBlock Text="{DynamicResource S.set.resolution}" Style="{DynamicResource SetLbl}"/>
<TextBlock Text="{DynamicResource S.set.resolution.sub}" Style="{DynamicResource SetSub}"/>
</StackPanel>
<ComboBox x:Name="SetRdpResolution" Grid.Column="1" Width="190" VerticalAlignment="Center">
<ComboBoxItem Content="{DynamicResource S.res.auto}" Tag=""/>
<ComboBoxItem Content="1024 × 768" Tag="1024x768"/>
<ComboBoxItem Content="1280 × 720" Tag="1280x720"/>
<ComboBoxItem Content="1280 × 1024" Tag="1280x1024"/>
<ComboBoxItem Content="1366 × 768" Tag="1366x768"/>
<ComboBoxItem Content="1600 × 900" Tag="1600x900"/>
<ComboBoxItem Content="1680 × 1050" Tag="1680x1050"/>
<ComboBoxItem Content="1920 × 1080" Tag="1920x1080"/>
<ComboBoxItem Content="1920 × 1200" Tag="1920x1200"/>
<ComboBoxItem Content="2560 × 1440" Tag="2560x1440"/>
<ComboBoxItem Content="3840 × 2160" Tag="3840x2160"/>
</ComboBox>
</Grid>
</Border>
<Border Style="{DynamicResource SetRow}">
<Grid>
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
<StackPanel VerticalAlignment="Center">
<TextBlock Text="{DynamicResource S.set.rdpscale}" Style="{DynamicResource SetLbl}"/>
<TextBlock Text="{DynamicResource S.set.rdpscale.sub}" Style="{DynamicResource SetSub}"/>
</StackPanel>
<ComboBox x:Name="SetRdpScale" Grid.Column="1" Width="190" VerticalAlignment="Center">
<ComboBoxItem Content="{DynamicResource S.scale.auto}" Tag=""/>
<ComboBoxItem Content="100%" Tag="100"/>
<ComboBoxItem Content="125%" Tag="125"/>
<ComboBoxItem Content="150%" Tag="150"/>
<ComboBoxItem Content="175%" Tag="175"/>
<ComboBoxItem Content="200%" Tag="200"/>
</ComboBox>
</Grid>
</Border>
<Border Style="{DynamicResource SetRow}">
<Grid>
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
Expand Down
23 changes: 23 additions & 0 deletions src/RdpManager/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -977,6 +977,8 @@ private void LoadSettingsForm()
BuildThemePresets();
BuildAccentSwatches();
SetDefaultPort.Text = _settings.DefaultPort.ToString();
ComboSelectTag(SetRdpResolution, RdpDisplay.FormatResolution(_settings.RdpDesktopWidth, _settings.RdpDesktopHeight));
ComboSelectTag(SetRdpScale, _settings.RdpScalePercent > 0 ? _settings.RdpScalePercent.ToString() : "");
SelectColorDepthSeg(_settings.ColorDepth);
SetRedirClip.IsChecked = _settings.DefaultRedirectClipboard;
SetRedirDrives.IsChecked = _settings.DefaultRedirectDrives;
Expand Down Expand Up @@ -1228,6 +1230,10 @@ private void SaveSettings_Click(object sender, RoutedEventArgs e)
_settings.TerminalFontSize = int.TryParse(SetTermFontSize.Text.Trim(), out var tfs) ? Math.Clamp(tfs, 8, 24) : 14;
_settings.DefaultPort = int.TryParse(SetDefaultPort.Text.Trim(), out var p) ? Math.Clamp(p, 1, 65535) : 3389;
_settings.ColorDepth = ParseColorDepth();
var (rdpW, rdpH) = RdpDisplay.ParseResolution(ComboTag(SetRdpResolution));
_settings.RdpDesktopWidth = rdpW;
_settings.RdpDesktopHeight = rdpH;
_settings.RdpScalePercent = RdpDisplay.ParseScale(ComboTag(SetRdpScale));
_settings.DefaultRedirectClipboard = SetRedirClip.IsChecked == true;
_settings.DefaultRedirectDrives = SetRedirDrives.IsChecked == true;
_settings.ProbeTimeoutSeconds = int.TryParse(SetProbeTimeout.Text.Trim(), out var pt) ? Math.Clamp(pt, 1, 60) : 2;
Expand Down Expand Up @@ -1261,6 +1267,18 @@ private void SaveSettings_Click(object sender, RoutedEventArgs e)

private int ParseColorDepth() => RdpUtils.ParseColorDepth(SegTag(ColorDepthSeg));

// Listy rozwijane z wartością w Tagu (rozdzielczość i skalowanie RDP) — odpowiednik SegTag/SegSet
// dla ComboBoxa. Brak dopasowania → pierwsza pozycja (u nas: „auto").
private static string ComboTag(ComboBox combo) => (combo?.SelectedItem as ComboBoxItem)?.Tag as string ?? "";

private static void ComboSelectTag(ComboBox combo, string tag)
{
if (combo == null) return;
foreach (var item in combo.Items.OfType<ComboBoxItem>())
if ((item.Tag as string ?? "") == (tag ?? "")) { combo.SelectedItem = item; return; }
combo.SelectedIndex = 0;
}

// Zaznacza segment głębi kolorów (16/24/32) wg wartości z ustawień.
private void SelectColorDepthSeg(int depth)
{
Expand All @@ -1277,6 +1295,11 @@ private void ApplySettings()
_fs.ApplyBarDelay(); // opóźnienie paska pełnoekranowego + peeku wg ustawień (PR 5)
_reach?.ApplySettings(); // limit czasu sondy + interwał + wł/wył cyklu wg ustawień

// Rozdzielczość / skalowanie RDP: renegocjuj w już otwartych sesjach (karty i osobne okna),
// żeby zmiana działała od razu, bez ponownego łączenia.
foreach (var s in _sessions) s.Resizer?.ApplyDisplaySettings();
foreach (var w in _sessionWindows) w.ApplyDisplaySettings();

// Cykliczne sprawdzanie aktualizacji wg tego samego przełącznika co start.
_update?.ApplyCheckUpdatesSetting();

Expand Down
4 changes: 3 additions & 1 deletion src/RdpManager/RdpConnect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ public static void Apply(AxMsRdpClient11NotSafeForScripting rdp, ServerInfo serv
adv.AuthenticationLevel = RdpConfigurator.ClampAuthLevel(server.AuthenticationLevel);
adv.EnableCredSspSupport = true;
adv.ConnectToAdministerServer = server.AdminSession; // sesja konsolowa (mstsc /admin)
adv.SmartSizing = false; // dynamiczna rozdzielczość zajmie się dopasowaniem
// Wartość wyjściowa; ostateczną ustala RdpDynamicResolution.ApplyPreConnect wg ustawień
// (stała rozdzielczość => SmartSizing, „dopasuj do okna" => dynamiczna rozdzielczość).
adv.SmartSizing = false;
adv.EnableAutoReconnect = settings.AutoReconnect;
rdp.ColorDepth = settings.ColorDepth;
adv.RedirectClipboard = server.RedirectClipboard;
Expand Down
Loading
Loading