From 4128f3404abc37a9d891daf1a2af9e9c6a0974c0 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 7 Oct 2025 19:31:35 +0000
Subject: [PATCH 01/11] Initial plan
From 4897771fbfb974a515310512e15ab6a97872ce88 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 7 Oct 2025 19:35:06 +0000
Subject: [PATCH 02/11] Initial plan
From e67754ee9ffc413de054fb52efdd537fdd4e3768 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 7 Oct 2025 19:35:18 +0000
Subject: [PATCH 03/11] Fix unhandled exception when closing panel with
connections
Co-authored-by: Kvarkas <3611964+Kvarkas@users.noreply.github.com>
---
mRemoteNG/UI/Window/ConnectionWindow.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/mRemoteNG/UI/Window/ConnectionWindow.cs b/mRemoteNG/UI/Window/ConnectionWindow.cs
index 4c1c1691..506e88e3 100644
--- a/mRemoteNG/UI/Window/ConnectionWindow.cs
+++ b/mRemoteNG/UI/Window/ConnectionWindow.cs
@@ -776,6 +776,7 @@ namespace mRemoteNG.UI.Window
ProtocolBase protocolBase = sender as ProtocolBase;
if (!(protocolBase?.InterfaceControl.Parent is ConnectionTab tabPage)) return;
if (tabPage.Disposing || tabPage.IsDisposed) return;
+ if (IsDisposed || Disposing) return;
tabPage.protocolClose = true;
Invoke(new Action(() => tabPage.Close()));
}
From 0f819ade56928a96bac2988a26b2cf569532b3f7 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 7 Oct 2025 19:39:14 +0000
Subject: [PATCH 04/11] Initial plan
From f8b7d37af1ff16d4b451ae00e49447d87b164323 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 7 Oct 2025 19:42:31 +0000
Subject: [PATCH 05/11] Add ARD (Apple Remote Desktop) protocol support
Co-authored-by: Kvarkas <3611964+Kvarkas@users.noreply.github.com>
---
.../Connection/Protocol/ARD/ProtocolARD.cs | 19 +++++++++++++++++++
.../Connection/Protocol/ProtocolFactory.cs | 3 +++
mRemoteNG/Connection/Protocol/ProtocolType.cs | 3 +++
mRemoteNG/Language/Language.resx | 3 +++
4 files changed, 28 insertions(+)
create mode 100644 mRemoteNG/Connection/Protocol/ARD/ProtocolARD.cs
diff --git a/mRemoteNG/Connection/Protocol/ARD/ProtocolARD.cs b/mRemoteNG/Connection/Protocol/ARD/ProtocolARD.cs
new file mode 100644
index 00000000..d10a7ff9
--- /dev/null
+++ b/mRemoteNG/Connection/Protocol/ARD/ProtocolARD.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Runtime.Versioning;
+using mRemoteNG.Connection.Protocol.VNC;
+
+namespace mRemoteNG.Connection.Protocol.ARD
+{
+ [SupportedOSPlatform("windows")]
+ public class ProtocolARD : ProtocolVNC
+ {
+ public ProtocolARD()
+ {
+ }
+
+ public new enum Defaults
+ {
+ Port = 5900
+ }
+ }
+}
diff --git a/mRemoteNG/Connection/Protocol/ProtocolFactory.cs b/mRemoteNG/Connection/Protocol/ProtocolFactory.cs
index 820724f8..b6d7fbba 100644
--- a/mRemoteNG/Connection/Protocol/ProtocolFactory.cs
+++ b/mRemoteNG/Connection/Protocol/ProtocolFactory.cs
@@ -5,6 +5,7 @@ using mRemoteNG.Connection.Protocol.Rlogin;
using mRemoteNG.Connection.Protocol.SSH;
using mRemoteNG.Connection.Protocol.Telnet;
using mRemoteNG.Connection.Protocol.VNC;
+using mRemoteNG.Connection.Protocol.ARD;
using System;
using mRemoteNG.Connection.Protocol.PowerShell;
using mRemoteNG.Resources.Language;
@@ -28,6 +29,8 @@ namespace mRemoteNG.Connection.Protocol
return rdp;
case ProtocolType.VNC:
return new ProtocolVNC();
+ case ProtocolType.ARD:
+ return new ProtocolARD();
case ProtocolType.SSH1:
return new ProtocolSSH1();
case ProtocolType.SSH2:
diff --git a/mRemoteNG/Connection/Protocol/ProtocolType.cs b/mRemoteNG/Connection/Protocol/ProtocolType.cs
index f4e1bd47..c795cd8e 100644
--- a/mRemoteNG/Connection/Protocol/ProtocolType.cs
+++ b/mRemoteNG/Connection/Protocol/ProtocolType.cs
@@ -35,6 +35,9 @@ namespace mRemoteNG.Connection.Protocol
[LocalizedAttributes.LocalizedDescription(nameof(Language.PowerShell))]
PowerShell = 10,
+ [LocalizedAttributes.LocalizedDescription(nameof(Language.Ard))]
+ ARD = 11,
+
[LocalizedAttributes.LocalizedDescription(nameof(Language.ExternalTool))]
IntApp = 20
}
diff --git a/mRemoteNG/Language/Language.resx b/mRemoteNG/Language/Language.resx
index 311c87c4..8382b882 100644
--- a/mRemoteNG/Language/Language.resx
+++ b/mRemoteNG/Language/Language.resx
@@ -159,6 +159,9 @@
Automatic update settings
+
+ ARD (Apple Remote Desktop)
+
Aspect
From b19319926850ffd8fd20bf4cedb28c09886ac52e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 7 Oct 2025 19:45:38 +0000
Subject: [PATCH 06/11] Add ARD protocol to VNC property attributes and default
port handling
Co-authored-by: Kvarkas <3611964+Kvarkas@users.noreply.github.com>
---
.../Connection/AbstractConnectionRecord.cs | 22 +++++++++----------
mRemoteNG/Connection/ConnectionInfo.cs | 3 +++
2 files changed, 14 insertions(+), 11 deletions(-)
diff --git a/mRemoteNG/Connection/AbstractConnectionRecord.cs b/mRemoteNG/Connection/AbstractConnectionRecord.cs
index 74ff6e15..df8bbef5 100644
--- a/mRemoteNG/Connection/AbstractConnectionRecord.cs
+++ b/mRemoteNG/Connection/AbstractConnectionRecord.cs
@@ -908,7 +908,7 @@ namespace mRemoteNG.Connection
LocalizedAttributes.LocalizedDisplayName(nameof(Language.Compression)),
LocalizedAttributes.LocalizedDescription(nameof(Language.PropertyDescriptionCompression)),
TypeConverter(typeof(MiscTools.EnumTypeConverter)),
- AttributeUsedInProtocol(ProtocolType.VNC),
+ AttributeUsedInProtocol(ProtocolType.VNC, ProtocolType.ARD),
Browsable(false)]
public ProtocolVNC.Compression VNCCompression
{
@@ -920,7 +920,7 @@ namespace mRemoteNG.Connection
LocalizedAttributes.LocalizedDisplayName(nameof(Language.Encoding)),
LocalizedAttributes.LocalizedDescription(nameof(Language.PropertyDescriptionEncoding)),
TypeConverter(typeof(MiscTools.EnumTypeConverter)),
- AttributeUsedInProtocol(ProtocolType.VNC),
+ AttributeUsedInProtocol(ProtocolType.VNC, ProtocolType.ARD),
Browsable(false)]
public ProtocolVNC.Encoding VNCEncoding
{
@@ -932,7 +932,7 @@ namespace mRemoteNG.Connection
LocalizedAttributes.LocalizedDisplayName(nameof(Language.AuthenticationMode)),
LocalizedAttributes.LocalizedDescription(nameof(Language.PropertyDescriptionAuthenticationMode)),
TypeConverter(typeof(MiscTools.EnumTypeConverter)),
- AttributeUsedInProtocol(ProtocolType.VNC),
+ AttributeUsedInProtocol(ProtocolType.VNC, ProtocolType.ARD),
Browsable(false)]
public ProtocolVNC.AuthMode VNCAuthMode
{
@@ -944,7 +944,7 @@ namespace mRemoteNG.Connection
LocalizedAttributes.LocalizedDisplayName(nameof(Language.ProxyType)),
LocalizedAttributes.LocalizedDescription(nameof(Language.PropertyDescriptionVNCProxyType)),
TypeConverter(typeof(MiscTools.EnumTypeConverter)),
- AttributeUsedInProtocol(ProtocolType.VNC),
+ AttributeUsedInProtocol(ProtocolType.VNC, ProtocolType.ARD),
Browsable(false)]
public ProtocolVNC.ProxyType VNCProxyType
{
@@ -955,7 +955,7 @@ namespace mRemoteNG.Connection
[LocalizedAttributes.LocalizedCategory(nameof(Language.Proxy), 7),
LocalizedAttributes.LocalizedDisplayName(nameof(Language.ProxyAddress)),
LocalizedAttributes.LocalizedDescription(nameof(Language.PropertyDescriptionVNCProxyAddress)),
- AttributeUsedInProtocol(ProtocolType.VNC),
+ AttributeUsedInProtocol(ProtocolType.VNC, ProtocolType.ARD),
Browsable(false)]
public string VNCProxyIP
{
@@ -966,7 +966,7 @@ namespace mRemoteNG.Connection
[LocalizedAttributes.LocalizedCategory(nameof(Language.Proxy), 7),
LocalizedAttributes.LocalizedDisplayName(nameof(Language.ProxyPort)),
LocalizedAttributes.LocalizedDescription(nameof(Language.PropertyDescriptionVNCProxyPort)),
- AttributeUsedInProtocol(ProtocolType.VNC),
+ AttributeUsedInProtocol(ProtocolType.VNC, ProtocolType.ARD),
Browsable(false)]
public int VNCProxyPort
{
@@ -977,7 +977,7 @@ namespace mRemoteNG.Connection
[LocalizedAttributes.LocalizedCategory(nameof(Language.Proxy), 7),
LocalizedAttributes.LocalizedDisplayName(nameof(Language.ProxyUsername)),
LocalizedAttributes.LocalizedDescription(nameof(Language.PropertyDescriptionVNCProxyUsername)),
- AttributeUsedInProtocol(ProtocolType.VNC),
+ AttributeUsedInProtocol(ProtocolType.VNC, ProtocolType.ARD),
Browsable(false)]
public string VNCProxyUsername
{
@@ -989,7 +989,7 @@ namespace mRemoteNG.Connection
LocalizedAttributes.LocalizedDisplayName(nameof(Language.ProxyPassword)),
LocalizedAttributes.LocalizedDescription(nameof(Language.PropertyDescriptionVNCProxyPassword)),
PasswordPropertyText(true),
- AttributeUsedInProtocol(ProtocolType.VNC),
+ AttributeUsedInProtocol(ProtocolType.VNC, ProtocolType.ARD),
Browsable(false)]
public string VNCProxyPassword
{
@@ -1001,7 +1001,7 @@ namespace mRemoteNG.Connection
LocalizedAttributes.LocalizedDisplayName(nameof(Language.Colors)),
LocalizedAttributes.LocalizedDescription(nameof(Language.PropertyDescriptionColors)),
TypeConverter(typeof(MiscTools.EnumTypeConverter)),
- AttributeUsedInProtocol(ProtocolType.VNC),
+ AttributeUsedInProtocol(ProtocolType.VNC, ProtocolType.ARD),
Browsable(false)]
public ProtocolVNC.Colors VNCColors
{
@@ -1013,7 +1013,7 @@ namespace mRemoteNG.Connection
LocalizedAttributes.LocalizedDisplayName(nameof(Language.SmartSizeMode)),
LocalizedAttributes.LocalizedDescription(nameof(Language.PropertyDescriptionSmartSizeMode)),
TypeConverter(typeof(MiscTools.EnumTypeConverter)),
- AttributeUsedInProtocol(ProtocolType.VNC)]
+ AttributeUsedInProtocol(ProtocolType.VNC, ProtocolType.ARD)]
public ProtocolVNC.SmartSizeMode VNCSmartSizeMode
{
get => GetPropertyValue("VNCSmartSizeMode", _vncSmartSizeMode);
@@ -1024,7 +1024,7 @@ namespace mRemoteNG.Connection
LocalizedAttributes.LocalizedDisplayName(nameof(Language.ViewOnly)),
LocalizedAttributes.LocalizedDescription(nameof(Language.PropertyDescriptionViewOnly)),
TypeConverter(typeof(MiscTools.YesNoTypeConverter)),
- AttributeUsedInProtocol(ProtocolType.VNC)]
+ AttributeUsedInProtocol(ProtocolType.VNC, ProtocolType.ARD)]
public bool VNCViewOnly
{
get => GetPropertyValue("VNCViewOnly", _vncViewOnly);
diff --git a/mRemoteNG/Connection/ConnectionInfo.cs b/mRemoteNG/Connection/ConnectionInfo.cs
index f6538436..3bb9a848 100644
--- a/mRemoteNG/Connection/ConnectionInfo.cs
+++ b/mRemoteNG/Connection/ConnectionInfo.cs
@@ -5,6 +5,7 @@ using System.Linq;
using System.Reflection;
using mRemoteNG.App;
using mRemoteNG.Connection.Protocol;
+using mRemoteNG.Connection.Protocol.ARD;
using mRemoteNG.Connection.Protocol.Http;
using mRemoteNG.Connection.Protocol.PowerShell;
using mRemoteNG.Connection.Protocol.RAW;
@@ -254,6 +255,8 @@ namespace mRemoteNG.Connection
return (int)RdpProtocol.Defaults.Port;
case ProtocolType.VNC:
return (int)ProtocolVNC.Defaults.Port;
+ case ProtocolType.ARD:
+ return (int)ProtocolARD.Defaults.Port;
case ProtocolType.SSH1:
return (int)ProtocolSSH1.Defaults.Port;
case ProtocolType.SSH2:
From ac4469bb4a36484a221b7c9cd20540207d0fe774 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 7 Oct 2025 19:48:57 +0000
Subject: [PATCH 07/11] Add ARD protocol support to UI, port scanning, and
tests
Co-authored-by: Kvarkas <3611964+Kvarkas@users.noreply.github.com>
---
.../Serializers/MiscSerializers/PortScanDeserializer.cs | 4 ++++
.../ConnectionInfoPropertyGrid/ConnectionInfoPropertyGrid.cs | 1 +
mRemoteNGTests/Connection/ConnectionInfoTests.cs | 1 +
3 files changed, 6 insertions(+)
diff --git a/mRemoteNG/Config/Serializers/MiscSerializers/PortScanDeserializer.cs b/mRemoteNG/Config/Serializers/MiscSerializers/PortScanDeserializer.cs
index f80081b8..4fc09ae2 100644
--- a/mRemoteNG/Config/Serializers/MiscSerializers/PortScanDeserializer.cs
+++ b/mRemoteNG/Config/Serializers/MiscSerializers/PortScanDeserializer.cs
@@ -61,6 +61,10 @@ namespace mRemoteNG.Config.Serializers.MiscSerializers
if (host.Vnc)
finalProtocol = ProtocolType.VNC;
break;
+ case ProtocolType.ARD:
+ if (host.Vnc)
+ finalProtocol = ProtocolType.ARD;
+ break;
default:
protocolValid = false;
break;
diff --git a/mRemoteNG/UI/Controls/ConnectionInfoPropertyGrid/ConnectionInfoPropertyGrid.cs b/mRemoteNG/UI/Controls/ConnectionInfoPropertyGrid/ConnectionInfoPropertyGrid.cs
index fb53dd2c..241c19d8 100644
--- a/mRemoteNG/UI/Controls/ConnectionInfoPropertyGrid/ConnectionInfoPropertyGrid.cs
+++ b/mRemoteNG/UI/Controls/ConnectionInfoPropertyGrid/ConnectionInfoPropertyGrid.cs
@@ -168,6 +168,7 @@ namespace mRemoteNG.UI.Controls.ConnectionInfoPropertyGrid
strHide.AddRange(SpecialRdpExclusions());
break;
case ProtocolType.VNC:
+ case ProtocolType.ARD:
strHide.AddRange(SpecialVncExclusions());
break;
}
diff --git a/mRemoteNGTests/Connection/ConnectionInfoTests.cs b/mRemoteNGTests/Connection/ConnectionInfoTests.cs
index 681f772d..6f8eeffe 100644
--- a/mRemoteNGTests/Connection/ConnectionInfoTests.cs
+++ b/mRemoteNGTests/Connection/ConnectionInfoTests.cs
@@ -115,6 +115,7 @@ namespace mRemoteNGTests.Connection
[TestCase(ProtocolType.SSH2, ExpectedResult = 22)]
[TestCase(ProtocolType.Telnet, ExpectedResult = 23)]
[TestCase(ProtocolType.VNC, ExpectedResult = 5900)]
+ [TestCase(ProtocolType.ARD, ExpectedResult = 5900)]
public int GetDefaultPortReturnsCorrectPortForProtocol(ProtocolType protocolType)
{
_connectionInfo.Protocol = protocolType;
From 3c6a4856476f0f1790c81cf624592299740b7f52 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 7 Oct 2025 19:49:32 +0000
Subject: [PATCH 08/11] Add TabColor property to connection info and implement
tab coloring
Co-authored-by: Kvarkas <3611964+Kvarkas@users.noreply.github.com>
---
.../Xml/XmlConnectionNodeSerializer28.cs | 3 ++
.../Xml/XmlConnectionsDeserializer.cs | 2 ++
.../Connection/AbstractConnectionRecord.cs | 12 +++++++
mRemoteNG/Connection/ConnectionInfo.cs | 1 +
.../Connection/ConnectionInfoInheritance.cs | 6 ++++
mRemoteNG/Language/Language.resx | 6 ++++
mRemoteNG/UI/Tabs/DockPaneStripNG.cs | 32 +++++++++++++++++--
7 files changed, 60 insertions(+), 2 deletions(-)
diff --git a/mRemoteNG/Config/Serializers/ConnectionSerializers/Xml/XmlConnectionNodeSerializer28.cs b/mRemoteNG/Config/Serializers/ConnectionSerializers/Xml/XmlConnectionNodeSerializer28.cs
index 06bb6124..a12d8ed7 100644
--- a/mRemoteNG/Config/Serializers/ConnectionSerializers/Xml/XmlConnectionNodeSerializer28.cs
+++ b/mRemoteNG/Config/Serializers/ConnectionSerializers/Xml/XmlConnectionNodeSerializer28.cs
@@ -42,6 +42,7 @@ namespace mRemoteNG.Config.Serializers.ConnectionSerializers.Xml
element.Add(new XAttribute("Descr", connectionInfo.Description));
element.Add(new XAttribute("Icon", connectionInfo.Icon));
element.Add(new XAttribute("Panel", connectionInfo.Panel));
+ element.Add(new XAttribute("TabColor", connectionInfo.TabColor));
element.Add(new XAttribute("Id", connectionInfo.ConstantID));
if (!Runtime.UseCredentialManager)
@@ -187,6 +188,8 @@ namespace mRemoteNG.Config.Serializers.ConnectionSerializers.Xml
element.Add(new XAttribute("InheritIcon", inheritance.Icon.ToString().ToLowerInvariant()));
if (inheritance.Panel)
element.Add(new XAttribute("InheritPanel", inheritance.Panel.ToString().ToLowerInvariant()));
+ if (inheritance.TabColor)
+ element.Add(new XAttribute("InheritTabColor", inheritance.TabColor.ToString().ToLowerInvariant()));
if (inheritance.Password)
element.Add(new XAttribute("InheritPassword", inheritance.Password.ToString().ToLowerInvariant()));
if (inheritance.Port)
diff --git a/mRemoteNG/Config/Serializers/ConnectionSerializers/Xml/XmlConnectionsDeserializer.cs b/mRemoteNG/Config/Serializers/ConnectionSerializers/Xml/XmlConnectionsDeserializer.cs
index ab6c15a0..7f053ea1 100644
--- a/mRemoteNG/Config/Serializers/ConnectionSerializers/Xml/XmlConnectionsDeserializer.cs
+++ b/mRemoteNG/Config/Serializers/ConnectionSerializers/Xml/XmlConnectionsDeserializer.cs
@@ -328,6 +328,7 @@ namespace mRemoteNG.Config.Serializers.ConnectionSerializers.Xml
connectionInfo.Inheritance.DisplayWallpaper = xmlnode.GetAttributeAsBool("InheritDisplayWallpaper");
connectionInfo.Inheritance.Icon = xmlnode.GetAttributeAsBool("InheritIcon");
connectionInfo.Inheritance.Panel = xmlnode.GetAttributeAsBool("InheritPanel");
+ connectionInfo.Inheritance.TabColor = xmlnode.GetAttributeAsBool("InheritTabColor");
connectionInfo.Inheritance.Port = xmlnode.GetAttributeAsBool("InheritPort");
connectionInfo.Inheritance.Protocol = xmlnode.GetAttributeAsBool("InheritProtocol");
connectionInfo.Inheritance.PuttySession = xmlnode.GetAttributeAsBool("InheritPuttySession");
@@ -350,6 +351,7 @@ namespace mRemoteNG.Config.Serializers.ConnectionSerializers.Xml
connectionInfo.Icon = xmlnode.GetAttributeAsString("Icon");
connectionInfo.Panel = xmlnode.GetAttributeAsString("Panel");
+ connectionInfo.TabColor = xmlnode.GetAttributeAsString("TabColor");
}
else
{
diff --git a/mRemoteNG/Connection/AbstractConnectionRecord.cs b/mRemoteNG/Connection/AbstractConnectionRecord.cs
index 74ff6e15..c16b0ec6 100644
--- a/mRemoteNG/Connection/AbstractConnectionRecord.cs
+++ b/mRemoteNG/Connection/AbstractConnectionRecord.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.ComponentModel;
+using System.Drawing;
using mRemoteNG.Connection.Protocol;
using mRemoteNG.Connection.Protocol.Http;
using mRemoteNG.Connection.Protocol.RDP;
@@ -22,6 +23,7 @@ namespace mRemoteNG.Connection
private string _description;
private string _icon;
private string _panel;
+ private string _tabColor;
private string _hostname;
private ExternalAddressProvider _externalAddressProvider;
@@ -153,6 +155,16 @@ namespace mRemoteNG.Connection
set => SetField(ref _panel, value, "Panel");
}
+ [LocalizedAttributes.LocalizedCategory(nameof(Language.Display)),
+ LocalizedAttributes.LocalizedDisplayName(nameof(Language.TabColor)),
+ LocalizedAttributes.LocalizedDescription(nameof(Language.PropertyDescriptionTabColor)),
+ TypeConverter(typeof(ColorConverter))]
+ public virtual string TabColor
+ {
+ get => GetPropertyValue("TabColor", _tabColor);
+ set => SetField(ref _tabColor, value, "TabColor");
+ }
+
#endregion
#region Connection
diff --git a/mRemoteNG/Connection/ConnectionInfo.cs b/mRemoteNG/Connection/ConnectionInfo.cs
index f6538436..0e02383a 100644
--- a/mRemoteNG/Connection/ConnectionInfo.cs
+++ b/mRemoteNG/Connection/ConnectionInfo.cs
@@ -289,6 +289,7 @@ namespace mRemoteNG.Connection
Description = Settings.Default.ConDefaultDescription;
Icon = Settings.Default.ConDefaultIcon;
Panel = Language.General;
+ TabColor = "";
}
private void SetConnectionDefaults()
diff --git a/mRemoteNG/Connection/ConnectionInfoInheritance.cs b/mRemoteNG/Connection/ConnectionInfoInheritance.cs
index 1c0e2281..ec2ca3ea 100644
--- a/mRemoteNG/Connection/ConnectionInfoInheritance.cs
+++ b/mRemoteNG/Connection/ConnectionInfoInheritance.cs
@@ -50,6 +50,12 @@ namespace mRemoteNG.Connection
TypeConverter(typeof(MiscTools.YesNoTypeConverter))]
public bool Panel { get; set; }
+ [LocalizedAttributes.LocalizedCategory(nameof(Language.Display), 2),
+ LocalizedAttributes.LocalizedDisplayNameInherit(nameof(Language.TabColor)),
+ LocalizedAttributes.LocalizedDescriptionInherit(nameof(Language.PropertyDescriptionTabColor)),
+ TypeConverter(typeof(MiscTools.YesNoTypeConverter))]
+ public bool TabColor { get; set; }
+
#endregion
#region Connection
diff --git a/mRemoteNG/Language/Language.resx b/mRemoteNG/Language/Language.resx
index 311c87c4..f4297a93 100644
--- a/mRemoteNG/Language/Language.resx
+++ b/mRemoteNG/Language/Language.resx
@@ -1020,6 +1020,9 @@ If you run into such an error, please create a new connection file!
Sets the panel in which the connection will open.
+
+ Sets the color of the connection tab. Leave empty for default theme color.
+
Enter your password.
@@ -1182,6 +1185,9 @@ If you run into such an error, please create a new connection file!
Panel
+
+ Tab Color
+
Password
diff --git a/mRemoteNG/UI/Tabs/DockPaneStripNG.cs b/mRemoteNG/UI/Tabs/DockPaneStripNG.cs
index a149ebbf..0360d64e 100644
--- a/mRemoteNG/UI/Tabs/DockPaneStripNG.cs
+++ b/mRemoteNG/UI/Tabs/DockPaneStripNG.cs
@@ -991,8 +991,11 @@ namespace mRemoteNG.UI.Tabs
rectText = DrawHelper.RtlTransform(this, rectText);
rectIcon = DrawHelper.RtlTransform(this, rectIcon);
- Color activeColor = DockPane.DockPanel.Theme.ColorPalette.TabSelectedActive.Background;
- Color lostFocusColor = DockPane.DockPanel.Theme.ColorPalette.TabSelectedInactive.Background;
+ // Get custom tab color if available
+ Color? customTabColor = GetCustomTabColor(tab.Content);
+
+ Color activeColor = customTabColor ?? DockPane.DockPanel.Theme.ColorPalette.TabSelectedActive.Background;
+ Color lostFocusColor = customTabColor ?? DockPane.DockPanel.Theme.ColorPalette.TabSelectedInactive.Background;
Color inactiveColor = DockPane.DockPanel.Theme.ColorPalette.MainWindowActive.Background;
Color mouseHoverColor = DockPane.DockPanel.Theme.ColorPalette.TabUnselectedHovered.Background;
@@ -1056,6 +1059,31 @@ namespace mRemoteNG.UI.Tabs
g.DrawIcon(tab.Content.DockHandler.Icon, rectIcon);
}
+ private Color? GetCustomTabColor(IDockContent content)
+ {
+ try
+ {
+ if (content is ConnectionTab connectionTab)
+ {
+ InterfaceControl interfaceControl = InterfaceControl.FindInterfaceControl(connectionTab);
+ if (interfaceControl?.Info != null)
+ {
+ string tabColorStr = interfaceControl.Info.TabColor;
+ if (!string.IsNullOrEmpty(tabColorStr))
+ {
+ ColorConverter converter = new ColorConverter();
+ return (Color)converter.ConvertFromString(tabColorStr);
+ }
+ }
+ }
+ }
+ catch
+ {
+ // If there's any error parsing the color, just return null to use default
+ }
+ return null;
+ }
+
private bool m_isMouseDown;
protected bool IsMouseDown
From 5830f39d50568073b954ed381b5a8ce9e39264b6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 7 Oct 2025 19:51:26 +0000
Subject: [PATCH 09/11] Add documentation for Tab Color feature
Co-authored-by: Kvarkas <3611964+Kvarkas@users.noreply.github.com>
---
CHANGELOG.md | 1 +
.../user_interface/connections.rst | 20 +++++++++++++++++++
2 files changed, 21 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1de0838a..e63bba60 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
- #2734: fix native build for Windows-x64
### Added
+- Add configurable connection tab colors to distinguish between different environments (e.g., Dev, Test, Live)
- #2728 Add support for building mRemoteNG on Windows ARM64
- #2723: Read keyboardhook, gatewayaccesstoken and gatewaycredentialssource from RDP File
- #2690: தமிழ் (ta) Translation update
diff --git a/mRemoteNGDocumentation/user_interface/connections.rst b/mRemoteNGDocumentation/user_interface/connections.rst
index 84aa31e4..763730ea 100644
--- a/mRemoteNGDocumentation/user_interface/connections.rst
+++ b/mRemoteNGDocumentation/user_interface/connections.rst
@@ -87,6 +87,26 @@ Icon
The icon indicates the visual identifier for the connection.
Clicking the icon will let you set a different icon for the connection.
+Tab Color
+---------
+
+.. note::
+
+ The Tab Color property is available in the Display category of the connection properties.
+
+You can set a custom color for connection tabs to help distinguish between different environments (e.g., Development, Testing, Production).
+This can be especially useful when working with critical systems like Live servers, where you want a clear visual reminder.
+
+To set a tab color:
+
+1. Select your connection in the Connections panel
+2. In the Config panel, expand the **Display** category
+3. Find the **Tab Color** property
+4. Enter a color name (e.g., "Red", "Green", "Blue") or a hex color code (e.g., "#FF0000", "#00FF00")
+5. Leave empty to use the default theme color
+
+The tab color will be applied when you open the connection. You can use inheritance to set the same color for multiple connections in a folder.
+
Status
------
From 5d623d80eb7eae3b28d7a2a9b68898281913f199 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 7 Oct 2025 20:10:54 +0000
Subject: [PATCH 10/11] Initial plan
From 347546ee0e580e843f4db520f72101900f1bb4d2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 7 Oct 2025 20:15:32 +0000
Subject: [PATCH 11/11] Add TabColorConverter to fix Color to String conversion
issue
Co-authored-by: Kvarkas <3611964+Kvarkas@users.noreply.github.com>
---
.../Connection/AbstractConnectionRecord.cs | 2 +-
mRemoteNG/Tools/MiscTools.cs | 126 +++++++++++++++
.../Tools/TabColorConverterTests.cs | 148 ++++++++++++++++++
3 files changed, 275 insertions(+), 1 deletion(-)
create mode 100644 mRemoteNGTests/Tools/TabColorConverterTests.cs
diff --git a/mRemoteNG/Connection/AbstractConnectionRecord.cs b/mRemoteNG/Connection/AbstractConnectionRecord.cs
index c16b0ec6..90e1a3db 100644
--- a/mRemoteNG/Connection/AbstractConnectionRecord.cs
+++ b/mRemoteNG/Connection/AbstractConnectionRecord.cs
@@ -158,7 +158,7 @@ namespace mRemoteNG.Connection
[LocalizedAttributes.LocalizedCategory(nameof(Language.Display)),
LocalizedAttributes.LocalizedDisplayName(nameof(Language.TabColor)),
LocalizedAttributes.LocalizedDescription(nameof(Language.PropertyDescriptionTabColor)),
- TypeConverter(typeof(ColorConverter))]
+ TypeConverter(typeof(MiscTools.TabColorConverter))]
public virtual string TabColor
{
get => GetPropertyValue("TabColor", _tabColor);
diff --git a/mRemoteNG/Tools/MiscTools.cs b/mRemoteNG/Tools/MiscTools.cs
index d35af82a..c1579691 100644
--- a/mRemoteNG/Tools/MiscTools.cs
+++ b/mRemoteNG/Tools/MiscTools.cs
@@ -268,5 +268,131 @@ namespace mRemoteNG.Tools
return svc;
}
}
+
+ public class TabColorConverter : TypeConverter
+ {
+ public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
+ {
+ return sourceType == typeof(string) || sourceType == typeof(Color) || base.CanConvertFrom(context, sourceType);
+ }
+
+ public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
+ {
+ return destinationType == typeof(string) || destinationType == typeof(Color) || base.CanConvertTo(context, destinationType);
+ }
+
+ public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object? value)
+ {
+ if (value == null || (value is string str && string.IsNullOrWhiteSpace(str)))
+ {
+ return string.Empty;
+ }
+
+ if (value is string stringValue)
+ {
+ return stringValue;
+ }
+
+ if (value is Color colorValue)
+ {
+ // Convert Color to string representation
+ // Use named color if it's a known color, otherwise use hex format
+ if (colorValue.IsNamedColor)
+ {
+ return colorValue.Name;
+ }
+ else
+ {
+ // Return hex format without alpha if fully opaque, otherwise include alpha
+ if (colorValue.A == 255)
+ {
+ return $"#{colorValue.R:X2}{colorValue.G:X2}{colorValue.B:X2}";
+ }
+ else
+ {
+ return $"#{colorValue.A:X2}{colorValue.R:X2}{colorValue.G:X2}{colorValue.B:X2}";
+ }
+ }
+ }
+
+ return base.ConvertFrom(context, culture, value);
+ }
+
+ public override object ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
+ {
+ if (destinationType == typeof(string))
+ {
+ if (value == null || (value is string str && string.IsNullOrWhiteSpace(str)))
+ {
+ return string.Empty;
+ }
+ return value.ToString() ?? string.Empty;
+ }
+
+ if (destinationType == typeof(Color))
+ {
+ if (value == null || (value is string str && string.IsNullOrWhiteSpace(str)))
+ {
+ return Color.Empty;
+ }
+
+ if (value is string stringValue)
+ {
+ try
+ {
+ ColorConverter converter = new ColorConverter();
+ return converter.ConvertFromString(stringValue) ?? Color.Empty;
+ }
+ catch
+ {
+ return Color.Empty;
+ }
+ }
+ }
+
+ return base.ConvertTo(context, culture, value, destinationType) ?? throw new InvalidOperationException("Base conversion returned null.");
+ }
+
+ public override bool GetStandardValuesSupported(ITypeDescriptorContext? context)
+ {
+ return true;
+ }
+
+ public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext? context)
+ {
+ // Provide a list of common colors for the dropdown
+ Color[] colors =
+ [
+ Color.Red,
+ Color.Orange,
+ Color.Yellow,
+ Color.Green,
+ Color.Blue,
+ Color.Purple,
+ Color.Pink,
+ Color.Brown,
+ Color.Black,
+ Color.White,
+ Color.Gray,
+ Color.LightGray,
+ Color.DarkGray,
+ Color.Cyan,
+ Color.Magenta,
+ Color.Lime,
+ Color.Navy,
+ Color.Teal,
+ Color.Maroon,
+ Color.Olive
+ ];
+
+ return new StandardValuesCollection(colors);
+ }
+
+ public override bool GetStandardValuesExclusive(ITypeDescriptorContext? context)
+ {
+ // Return false to allow custom values (hex codes or other color names)
+ return false;
+ }
+ }
}
}
\ No newline at end of file
diff --git a/mRemoteNGTests/Tools/TabColorConverterTests.cs b/mRemoteNGTests/Tools/TabColorConverterTests.cs
new file mode 100644
index 00000000..36c089d5
--- /dev/null
+++ b/mRemoteNGTests/Tools/TabColorConverterTests.cs
@@ -0,0 +1,148 @@
+using System.Drawing;
+using mRemoteNG.Tools;
+using NUnit.Framework;
+
+namespace mRemoteNGTests.Tools
+{
+ public class TabColorConverterTests
+ {
+ private MiscTools.TabColorConverter _converter;
+
+ [SetUp]
+ public void Setup()
+ {
+ _converter = new MiscTools.TabColorConverter();
+ }
+
+ [TestCase(typeof(string), true)]
+ [TestCase(typeof(Color), true)]
+ public void CanConvertFrom(Type typeToConvertFrom, bool expectedOutcome)
+ {
+ var actualOutcome = _converter.CanConvertFrom(typeToConvertFrom);
+ Assert.That(actualOutcome, Is.EqualTo(expectedOutcome));
+ }
+
+ [TestCase(typeof(string), true)]
+ [TestCase(typeof(Color), true)]
+ public void CanConvertTo(Type typeToConvertTo, bool expectedOutcome)
+ {
+ var actualOutcome = _converter.CanConvertTo(typeToConvertTo);
+ Assert.That(actualOutcome, Is.EqualTo(expectedOutcome));
+ }
+
+ [Test]
+ public void ConvertFromColorToStringNamedColor()
+ {
+ var color = Color.Red;
+ var result = _converter.ConvertFrom(color);
+ Assert.That(result, Is.EqualTo("Red"));
+ }
+
+ [Test]
+ public void ConvertFromColorToStringCustomColor()
+ {
+ var color = Color.FromArgb(255, 128, 64, 32);
+ var result = _converter.ConvertFrom(color);
+ Assert.That(result, Is.EqualTo("#80401F"));
+ }
+
+ [Test]
+ public void ConvertFromColorToStringCustomColorWithAlpha()
+ {
+ var color = Color.FromArgb(128, 255, 0, 0);
+ var result = _converter.ConvertFrom(color);
+ Assert.That(result, Is.EqualTo("#80FF0000"));
+ }
+
+ [Test]
+ public void ConvertFromStringReturnsString()
+ {
+ var colorString = "Blue";
+ var result = _converter.ConvertFrom(colorString);
+ Assert.That(result, Is.EqualTo("Blue"));
+ }
+
+ [Test]
+ public void ConvertFromHexStringReturnsString()
+ {
+ var colorString = "#FF0000";
+ var result = _converter.ConvertFrom(colorString);
+ Assert.That(result, Is.EqualTo("#FF0000"));
+ }
+
+ [Test]
+ public void ConvertFromNullReturnsEmptyString()
+ {
+ var result = _converter.ConvertFrom(null);
+ Assert.That(result, Is.EqualTo(string.Empty));
+ }
+
+ [Test]
+ public void ConvertFromEmptyStringReturnsEmptyString()
+ {
+ var result = _converter.ConvertFrom("");
+ Assert.That(result, Is.EqualTo(string.Empty));
+ }
+
+ [Test]
+ public void ConvertToStringFromString()
+ {
+ var colorString = "Green";
+ var result = _converter.ConvertTo(colorString, typeof(string));
+ Assert.That(result, Is.EqualTo("Green"));
+ }
+
+ [Test]
+ public void ConvertToColorFromNamedString()
+ {
+ var colorString = "Red";
+ var result = _converter.ConvertTo(colorString, typeof(Color));
+ Assert.That(result, Is.EqualTo(Color.Red));
+ }
+
+ [Test]
+ public void ConvertToColorFromHexString()
+ {
+ var colorString = "#FF0000";
+ var result = _converter.ConvertTo(colorString, typeof(Color));
+ var expectedColor = Color.FromArgb(255, 255, 0, 0);
+ Assert.That(result, Is.EqualTo(expectedColor));
+ }
+
+ [Test]
+ public void ConvertToColorFromEmptyStringReturnsEmpty()
+ {
+ var result = _converter.ConvertTo("", typeof(Color));
+ Assert.That(result, Is.EqualTo(Color.Empty));
+ }
+
+ [Test]
+ public void ConvertToColorFromNullReturnsEmpty()
+ {
+ var result = _converter.ConvertTo(null, typeof(Color));
+ Assert.That(result, Is.EqualTo(Color.Empty));
+ }
+
+ [Test]
+ public void GetStandardValuesSupportedReturnsTrue()
+ {
+ var result = _converter.GetStandardValuesSupported(null);
+ Assert.That(result, Is.True);
+ }
+
+ [Test]
+ public void GetStandardValuesReturnsColorList()
+ {
+ var result = _converter.GetStandardValues(null);
+ Assert.That(result, Is.Not.Null);
+ Assert.That(result.Count, Is.GreaterThan(0));
+ }
+
+ [Test]
+ public void GetStandardValuesExclusiveReturnsFalse()
+ {
+ var result = _converter.GetStandardValuesExclusive(null);
+ Assert.That(result, Is.False);
+ }
+ }
+}