From 448b2affab6304366951561dd9e2dbae03fdab3e Mon Sep 17 00:00:00 2001
From: dearmb <153297994+dearmb@users.noreply.github.com>
Date: Mon, 10 Aug 2026 01:28:26 +0800
Subject: [PATCH 1/6] feat: support newline-separated hosts and shorthand
ranges
Host input (IP Scanner, Port Scanner, Ping Monitor) now accepts
newline-separated entries, so a column pasted from Excel works
directly (one IP/range per line). The existing semicolon-separated
format keeps working.
Shorthand IPv4 ranges like 192.168.0.1-100 (192.168.0.1 to
192.168.0.100) are now supported too, alongside the existing
full-range format 192.168.0.1-192.168.0.100.
- HostRangeHelper.CreateListFromInput: split on ';', CR, LF
- HostRangeHelper.ResolveAsync: expand shorthand ranges
- RegexHelper: add IPv4AddressShortRangeRegex
- MultipleHostsRangeValidator: accept newlines + shorthand ranges
- Docs: document both features
---
.../Network/HostRangeHelper.cs | 26 ++++++++++++++++---
.../NETworkManager.Utilities/RegexHelper.cs | 20 ++++++++++++--
.../MultipleHostsRangeValidator.cs | 18 ++++++++++++-
Website/docs/application/ip-scanner.md | 4 ++-
Website/docs/application/ping-monitor.md | 4 ++-
Website/docs/application/port-scanner.md | 4 ++-
6 files changed, 66 insertions(+), 10 deletions(-)
diff --git a/Source/NETworkManager.Models/Network/HostRangeHelper.cs b/Source/NETworkManager.Models/Network/HostRangeHelper.cs
index f7b21cadef..e2ddeffde1 100644
--- a/Source/NETworkManager.Models/Network/HostRangeHelper.cs
+++ b/Source/NETworkManager.Models/Network/HostRangeHelper.cs
@@ -1,4 +1,5 @@
using NETworkManager.Utilities;
+using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
@@ -17,14 +18,15 @@ namespace NETworkManager.Models.Network;
public static class HostRangeHelper
{
///
- /// Create a list of hosts from a string input like "10.0.0.1; example.com; 10.0.0.0/24"
+ /// Create a list of hosts from a string input like "10.0.0.1; example.com; 10.0.0.0/24".
+ /// Inputs can also be separated by newlines (e.g. pasted from Excel, one host/range per line).
///
- /// Hosts like "10.0.0.1; example.com; 10.0.0.0/24"
+ /// Hosts like "10.0.0.1; example.com; 10.0.0.0/24" or newline-separated lines
/// List of hosts.
public static IEnumerable CreateListFromInput(string hosts)
{
- return hosts.Replace(" ", "").Split(';')
- .Where(x => !string.IsNullOrEmpty(x))
+ return hosts.Replace(" ", "")
+ .Split([';', '\r', '\n'], StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Trim())
.ToArray();
}
@@ -65,6 +67,22 @@ public static IEnumerable CreateListFromInput(string hosts)
break;
+ // 192.168.0.1-100
+ case var _ when RegexHelper.IPv4AddressShortRangeRegex().IsMatch(host):
+ var shortRange = host.Split('-');
+ var shortBase = shortRange[0][..shortRange[0].LastIndexOf('.')];
+
+ Parallel.For(IPv4Address.ToInt32(IPAddress.Parse(shortRange[0])),
+ IPv4Address.ToInt32(IPAddress.Parse($"{shortBase}.{shortRange[1]}")) + 1, (i, state) =>
+ {
+ if (ct.IsCancellationRequested)
+ state.Break();
+
+ hostsBag.Add((IPv4Address.FromInt32(i), string.Empty));
+ });
+
+ break;
+
// 192.168.0.0 - 192.168.0.100
case var _ when RegexHelper.IPv4AddressRangeRegex().IsMatch(host):
var range = host.Split('-');
diff --git a/Source/NETworkManager.Utilities/RegexHelper.cs b/Source/NETworkManager.Utilities/RegexHelper.cs
index 0785fc2fa6..8462c2eedf 100644
--- a/Source/NETworkManager.Utilities/RegexHelper.cs
+++ b/Source/NETworkManager.Utilities/RegexHelper.cs
@@ -43,14 +43,30 @@ public static partial class RegexHelper
public static partial Regex IPv4AddressExtractRegex();
///
- /// Provides a compiles regular expression that matches IPv4 address ranges in the format "start-end" like
+ /// Represents a regular expression pattern that matches valid shorthand IPv4 address ranges like
+ /// "192.168.178.1-100" (base IP + last octet range).
+ ///
+ private const string IPv4AddressShortRangeValues =
+ @"((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\-(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)";
+
+ ///
+ /// Provides a compiled regular expression that matches IPv4 address ranges in the format "start-end" like
/// "192.168.178.0-192.168.178.255".
- ///
+ ///
/// A instance that matches strings representing IPv4 address ranges, such as
/// "192.168.1.1-192.168.1.100".
[GeneratedRegex($"^{IPv4AddressValues}-{IPv4AddressValues}$")]
public static partial Regex IPv4AddressRangeRegex();
+ ///
+ /// Provides a compiled regular expression that matches shorthand IPv4 address ranges like
+ /// "192.168.178.1-100" (base IP followed by a last-octet range).
+ ///
+ /// A instance that matches strings representing shorthand IPv4 address ranges,
+ /// such as "192.168.1.1-100" (192.168.1.1 to 192.168.1.100).
+ [GeneratedRegex($"^{IPv4AddressShortRangeValues}$")]
+ public static partial Regex IPv4AddressShortRangeRegex();
+
///
/// Provides a compiled regular expression that matches valid IPv4 subnet mask like "255.255.0.0".
///
diff --git a/Source/NETworkManager.Validators/MultipleHostsRangeValidator.cs b/Source/NETworkManager.Validators/MultipleHostsRangeValidator.cs
index a2674ce8ae..7f79762569 100644
--- a/Source/NETworkManager.Validators/MultipleHostsRangeValidator.cs
+++ b/Source/NETworkManager.Validators/MultipleHostsRangeValidator.cs
@@ -1,6 +1,7 @@
using NETworkManager.Localization.Resources;
using NETworkManager.Models.Network;
using NETworkManager.Utilities;
+using System;
using System.DirectoryServices.ActiveDirectory;
using System.Globalization;
using System.Net;
@@ -18,7 +19,9 @@ public override ValidationResult Validate(object value, CultureInfo cultureInfo)
if (value == null)
return new ValidationResult(false, Strings.EnterValidIPScanRange);
- foreach (var ipHostOrRange in ((string)value).Replace(" ", "").Split(';'))
+ foreach (var ipHostOrRange in ((string)value)
+ .Replace(" ", "")
+ .Split([';', '\r', '\n'], StringSplitOptions.RemoveEmptyEntries))
{
// 192.168.0.1
if (RegexHelper.IPv4AddressRegex().IsMatch(ipHostOrRange))
@@ -32,6 +35,19 @@ public override ValidationResult Validate(object value, CultureInfo cultureInfo)
if (RegexHelper.IPv4AddressSubnetmaskRegex().IsMatch(ipHostOrRange))
continue;
+ // 192.168.0.1-100
+ if (RegexHelper.IPv4AddressShortRangeRegex().IsMatch(ipHostOrRange))
+ {
+ var shortRange = ipHostOrRange.Split('-');
+ var shortBase = shortRange[0][..shortRange[0].LastIndexOf('.')];
+
+ if (IPv4Address.ToInt32(IPAddress.Parse(shortRange[0])) >
+ IPv4Address.ToInt32(IPAddress.Parse($"{shortBase}.{shortRange[1]}")))
+ isValid = false;
+
+ continue;
+ }
+
// 192.168.0.0 - 192.168.0.100
if (RegexHelper.IPv4AddressRangeRegex().IsMatch(ipHostOrRange))
{
diff --git a/Website/docs/application/ip-scanner.md b/Website/docs/application/ip-scanner.md
index 4f41a2f5d7..f2c12a0bd7 100644
--- a/Website/docs/application/ip-scanner.md
+++ b/Website/docs/application/ip-scanner.md
@@ -34,10 +34,12 @@ With the **IP Scanner** you can scan for active devices based on the hostname or
:::note
-Multiple inputs can be combined with a semicolon (`;`).
+Multiple inputs can be combined with a semicolon (`;`) or separated by newlines (one input per line, e.g. pasted from Excel).
Example: `10.0.0.0/24; 10.0.[10-20]1`
+Shorthand ranges like `192.168.0.1-100` (192.168.0.1 to 192.168.0.100) are also supported.
+
:::
### Toolbar
diff --git a/Website/docs/application/ping-monitor.md b/Website/docs/application/ping-monitor.md
index a1ad0a605d..248c349535 100644
--- a/Website/docs/application/ping-monitor.md
+++ b/Website/docs/application/ping-monitor.md
@@ -31,10 +31,12 @@ ICMP (Internet Control Message Protocol) is a network-layer protocol used to sen
:::note
-Multiple inputs can be combined with a semicolon (`;`).
+Multiple inputs can be combined with a semicolon (`;`) or separated by newlines (one input per line, e.g. pasted from Excel).
Example: `10.0.0.0/24; 10.0.[10-20]1`
+Shorthand ranges like `192.168.0.1-100` (192.168.0.1 to 192.168.0.100) are also supported.
+
:::
### Chart
diff --git a/Website/docs/application/port-scanner.md b/Website/docs/application/port-scanner.md
index 96d7632aca..18fe83de42 100644
--- a/Website/docs/application/port-scanner.md
+++ b/Website/docs/application/port-scanner.md
@@ -45,10 +45,12 @@ TCP (Transmission Control Protocol) is a connection-oriented transport-layer pro
:::note
-Multiple inputs can be combined with a semicolon (`;`).
+Multiple inputs can be combined with a semicolon (`;`) or separated by newlines (one input per line, e.g. pasted from Excel).
Example: `10.0.0.0/24; 10.0.[10-20]1` or `1-1024; 8080; 8443`
+Shorthand ranges like `192.168.0.1-100` (192.168.0.1 to 192.168.0.100) are also supported.
+
:::
### Toolbar
From 42aed2b02812044960a99dc3d1ee001397116985 Mon Sep 17 00:00:00 2001
From: dearmb <153297994+dearmb@users.noreply.github.com>
Date: Mon, 10 Aug 2026 01:30:26 +0800
Subject: [PATCH 2/6] docs: add changelog entry for newline-separated hosts and
shorthand ranges
Co-Authored-By: Claude
---
Website/docs/changelog/next-release.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/Website/docs/changelog/next-release.md b/Website/docs/changelog/next-release.md
index 81231a080f..11ddf144a0 100644
--- a/Website/docs/changelog/next-release.md
+++ b/Website/docs/changelog/next-release.md
@@ -33,6 +33,10 @@ Release date: **xx.xx.2026**
- The collapsed/expanded state of profile groups (e.g. **linux-server**) is now remembered per profile file and shared across all tools, instead of resetting every time you switch tools or restart the application. [#3539](https://github.com/BornToBeRoot/NETworkManager/pull/3539)
+**IP Scanner, Port Scanner & Ping Monitor**
+
+- Host input fields now accept newline-separated hosts (one per line, e.g. pasted from Excel) in addition to the existing semicolon (`;`) separator. Shorthand IPv4 ranges like `192.168.0.1-100` are supported as well. [#3568](https://github.com/BornToBeRoot/NETworkManager/pull/3568)
+
**IP Scanner**
- Added `135` (RPC) and `9100` (raw printing) to the default **Ports** list used to detect if a host is reachable. [#3564](https://github.com/BornToBeRoot/NETworkManager/pull/3564)
From 67aef5cf59cb5cfd0f0914a77b2dd312cdb8ec72 Mon Sep 17 00:00:00 2001
From: dearmb <153297994+dearmb@users.noreply.github.com>
Date: Mon, 10 Aug 2026 02:02:52 +0800
Subject: [PATCH 3/6] feat: convert multiline clipboard content to
semicolon-separated on paste
The WPF TextBox inside an editable ComboBox has AcceptsReturn=false, so pasting
multiline text (e.g. a column from Excel) silently drops everything after the
first line. Add a ComboBoxPasteBehavior attached property that rewrites the
clipboard content to the semicolon-separated form in CommandManager.PreviewExecuted,
before the paste command runs. Applied to the host input of IP Scanner, Port
Scanner and Ping Monitor.
Co-Authored-By: Claude
---
.../ComboBoxPasteBehavior.cs | 68 +++++++++++++++++++
.../NETworkManager/Views/IPScannerView.xaml | 1 +
.../Views/PingMonitorHostView.xaml | 1 +
.../NETworkManager/Views/PortScannerView.xaml | 1 +
Website/docs/application/ip-scanner.md | 2 +-
Website/docs/application/ping-monitor.md | 2 +-
Website/docs/application/port-scanner.md | 2 +-
7 files changed, 74 insertions(+), 3 deletions(-)
create mode 100644 Source/NETworkManager.Controls/ComboBoxPasteBehavior.cs
diff --git a/Source/NETworkManager.Controls/ComboBoxPasteBehavior.cs b/Source/NETworkManager.Controls/ComboBoxPasteBehavior.cs
new file mode 100644
index 0000000000..0e5598b159
--- /dev/null
+++ b/Source/NETworkManager.Controls/ComboBoxPasteBehavior.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Linq;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+
+namespace NETworkManager.Controls;
+
+///
+/// Attached property that converts multiline text (e.g. a column pasted from Excel) into a
+/// semicolon-separated single line when pasted into an editable .
+///
+/// Without this the WPF TextBox inside the ComboBox ( is
+/// false) silently drops everything after the first line when multiline text is pasted.
+/// The conversion happens in before the paste
+/// command actually runs, by rewriting the clipboard content to the semicolon-separated form.
+///
+public static class ComboBoxPasteBehavior
+{
+ public static readonly DependencyProperty ConvertMultilineToSemicolonProperty =
+ DependencyProperty.RegisterAttached(
+ "ConvertMultilineToSemicolon",
+ typeof(bool),
+ typeof(ComboBoxPasteBehavior),
+ new PropertyMetadata(false, OnConvertMultilineToSemicolonChanged));
+
+ public static void SetConvertMultilineToSemicolon(UIElement element, bool value) =>
+ element.SetValue(ConvertMultilineToSemicolonProperty, value);
+
+ public static bool GetConvertMultilineToSemicolon(UIElement element) =>
+ (bool)element.GetValue(ConvertMultilineToSemicolonProperty);
+
+ private static void OnConvertMultilineToSemicolonChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is not ComboBox comboBox)
+ return;
+
+ if ((bool)e.NewValue)
+ CommandManager.AddPreviewExecutedHandler(comboBox, OnPreviewExecuted);
+ else
+ CommandManager.RemovePreviewExecutedHandler(comboBox, OnPreviewExecuted);
+ }
+
+ private static void OnPreviewExecuted(object sender, ExecutedRoutedEventArgs e)
+ {
+ // Keyboard shortcut (Ctrl+V) and the paste context menu item both route through
+ // ApplicationCommands.Paste. Editing commands don't need to be handled here.
+ if (e.Command != ApplicationCommands.Paste)
+ return;
+
+ if (!Clipboard.ContainsText())
+ return;
+
+ var text = Clipboard.GetText();
+
+ // Only rewrite when there is actual multiline content (e.g. a column pasted from Excel).
+ if (!text.Contains('\n') && !text.Contains('\r'))
+ return;
+
+ var converted = string.Join(";", text
+ .Replace("\r\n", "\n")
+ .Replace('\r', '\n')
+ .Split('\n', StringSplitOptions.RemoveEmptyEntries)
+ .Select(x => x.Trim()));
+
+ Clipboard.SetText(converted);
+ }
+}
diff --git a/Source/NETworkManager/Views/IPScannerView.xaml b/Source/NETworkManager/Views/IPScannerView.xaml
index c60cf86ed4..0a85dce0ba 100644
--- a/Source/NETworkManager/Views/IPScannerView.xaml
+++ b/Source/NETworkManager/Views/IPScannerView.xaml
@@ -68,6 +68,7 @@
ItemsSource="{Binding Path=HostHistoryView}"
mah:TextBoxHelper.Watermark="{x:Static Member=localization:StaticStrings.ExampleHostRange}"
IsReadOnly="{Binding Path=IsRunning}"
+ controls:ComboBoxPasteBehavior.ConvertMultilineToSemicolon="True"
Style="{StaticResource ResourceKey=HistoryComboBox}">
diff --git a/Source/NETworkManager/Views/PingMonitorHostView.xaml b/Source/NETworkManager/Views/PingMonitorHostView.xaml
index 0d11f8f732..8971374365 100644
--- a/Source/NETworkManager/Views/PingMonitorHostView.xaml
+++ b/Source/NETworkManager/Views/PingMonitorHostView.xaml
@@ -97,6 +97,7 @@
ItemsSource="{Binding Path=HostHistoryView}"
mah:TextBoxHelper.Watermark="{x:Static Member=localization:StaticStrings.ExampleHostRange}"
IsReadOnly="{Binding Path=IsRunning}"
+ controls:ComboBoxPasteBehavior.ConvertMultilineToSemicolon="True"
Style="{StaticResource ResourceKey=HistoryComboBox}">
diff --git a/Source/NETworkManager/Views/PortScannerView.xaml b/Source/NETworkManager/Views/PortScannerView.xaml
index 54bb2151fa..459df23e8b 100644
--- a/Source/NETworkManager/Views/PortScannerView.xaml
+++ b/Source/NETworkManager/Views/PortScannerView.xaml
@@ -60,6 +60,7 @@
ItemsSource="{Binding HostHistoryView}"
mah:TextBoxHelper.Watermark="{x:Static localization:StaticStrings.ExampleHostRange}"
IsReadOnly="{Binding Path=IsRunning}"
+ controls:ComboBoxPasteBehavior.ConvertMultilineToSemicolon="True"
Style="{StaticResource HistoryComboBox}">
diff --git a/Website/docs/application/ip-scanner.md b/Website/docs/application/ip-scanner.md
index f2c12a0bd7..930f248c29 100644
--- a/Website/docs/application/ip-scanner.md
+++ b/Website/docs/application/ip-scanner.md
@@ -34,7 +34,7 @@ With the **IP Scanner** you can scan for active devices based on the hostname or
:::note
-Multiple inputs can be combined with a semicolon (`;`) or separated by newlines (one input per line, e.g. pasted from Excel).
+Multiple inputs can be combined with a semicolon (`;`). A column pasted from Excel (one entry per line) is converted to the semicolon-separated form automatically.
Example: `10.0.0.0/24; 10.0.[10-20]1`
diff --git a/Website/docs/application/ping-monitor.md b/Website/docs/application/ping-monitor.md
index 248c349535..d6aa1a8bb5 100644
--- a/Website/docs/application/ping-monitor.md
+++ b/Website/docs/application/ping-monitor.md
@@ -31,7 +31,7 @@ ICMP (Internet Control Message Protocol) is a network-layer protocol used to sen
:::note
-Multiple inputs can be combined with a semicolon (`;`) or separated by newlines (one input per line, e.g. pasted from Excel).
+Multiple inputs can be combined with a semicolon (`;`). A column pasted from Excel (one entry per line) is converted to the semicolon-separated form automatically.
Example: `10.0.0.0/24; 10.0.[10-20]1`
diff --git a/Website/docs/application/port-scanner.md b/Website/docs/application/port-scanner.md
index 18fe83de42..5613179653 100644
--- a/Website/docs/application/port-scanner.md
+++ b/Website/docs/application/port-scanner.md
@@ -45,7 +45,7 @@ TCP (Transmission Control Protocol) is a connection-oriented transport-layer pro
:::note
-Multiple inputs can be combined with a semicolon (`;`) or separated by newlines (one input per line, e.g. pasted from Excel).
+Multiple inputs can be combined with a semicolon (`;`). A column pasted from Excel (one entry per line) is converted to the semicolon-separated form automatically.
Example: `10.0.0.0/24; 10.0.[10-20]1` or `1-1024; 8080; 8443`
From 64bf417b18763a489f301f77aeada064722f0bed Mon Sep 17 00:00:00 2001
From: dearmb <153297994+dearmb@users.noreply.github.com>
Date: Mon, 10 Aug 2026 12:20:41 +0800
Subject: [PATCH 4/6] feat: convert multiline clipboard content to
semicolon-separated on paste in port input
Apply the same paste conversion to the Ports field of the Port Scanner,
so a column of ports pasted from Excel works as well.
Co-Authored-By: Claude
---
Source/NETworkManager/Views/PortScannerView.xaml | 1 +
1 file changed, 1 insertion(+)
diff --git a/Source/NETworkManager/Views/PortScannerView.xaml b/Source/NETworkManager/Views/PortScannerView.xaml
index 459df23e8b..bbf3f36012 100644
--- a/Source/NETworkManager/Views/PortScannerView.xaml
+++ b/Source/NETworkManager/Views/PortScannerView.xaml
@@ -80,6 +80,7 @@
ItemsSource="{Binding PortsHistoryView}"
mah:TextBoxHelper.Watermark="{x:Static localization:StaticStrings.ExamplePortScanRange}"
IsReadOnly="{Binding Path=IsRunning}"
+ controls:ComboBoxPasteBehavior.ConvertMultilineToSemicolon="True"
Style="{StaticResource HistoryComboBox}">
From c22435841970011ea20d3c9fd02ad9084ef3eabe Mon Sep 17 00:00:00 2001
From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com>
Date: Sat, 15 Aug 2026 02:03:28 +0200
Subject: [PATCH 5/6] Fix: guard ComboBoxPasteBehavior clipboard access,
document shorthand IPv4 ranges as example input
---
.../ComboBoxPasteBehavior.cs | 47 ++++++++++++++++---
.../Resources/StaticStrings.Designer.cs | 2 +-
.../Resources/StaticStrings.resx | 2 +-
Website/docs/application/ip-scanner.md | 5 +-
Website/docs/application/ping-monitor.md | 5 +-
Website/docs/application/port-scanner.md | 5 +-
Website/docs/changelog/next-release.md | 13 +++--
7 files changed, 58 insertions(+), 21 deletions(-)
diff --git a/Source/NETworkManager.Controls/ComboBoxPasteBehavior.cs b/Source/NETworkManager.Controls/ComboBoxPasteBehavior.cs
index 0e5598b159..87397e2f65 100644
--- a/Source/NETworkManager.Controls/ComboBoxPasteBehavior.cs
+++ b/Source/NETworkManager.Controls/ComboBoxPasteBehavior.cs
@@ -1,8 +1,10 @@
using System;
using System.Linq;
+using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
+using System.Windows.Threading;
namespace NETworkManager.Controls;
@@ -14,6 +16,8 @@ namespace NETworkManager.Controls;
/// false) silently drops everything after the first line when multiline text is pasted.
/// The conversion happens in before the paste
/// command actually runs, by rewriting the clipboard content to the semicolon-separated form.
+/// The original clipboard content is restored afterwards, and clipboard access is guarded since
+/// the Windows clipboard can throw when briefly locked by another process.
///
public static class ComboBoxPasteBehavior
{
@@ -48,21 +52,52 @@ private static void OnPreviewExecuted(object sender, ExecutedRoutedEventArgs e)
if (e.Command != ApplicationCommands.Paste)
return;
- if (!Clipboard.ContainsText())
- return;
+ string originalText;
+
+ try
+ {
+ if (!Clipboard.ContainsText())
+ return;
- var text = Clipboard.GetText();
+ originalText = Clipboard.GetText();
+ }
+ catch (ExternalException)
+ {
+ // Clipboard is temporarily locked by another process - fall back to the default paste.
+ return;
+ }
// Only rewrite when there is actual multiline content (e.g. a column pasted from Excel).
- if (!text.Contains('\n') && !text.Contains('\r'))
+ if (!originalText.Contains('\n') && !originalText.Contains('\r'))
return;
- var converted = string.Join(";", text
+ var converted = string.Join(";", originalText
.Replace("\r\n", "\n")
.Replace('\r', '\n')
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Trim()));
- Clipboard.SetText(converted);
+ try
+ {
+ Clipboard.SetText(converted);
+ }
+ catch (ExternalException)
+ {
+ return;
+ }
+
+ // Restore the original clipboard content once the paste command has consumed the
+ // rewritten text, so pasting elsewhere afterwards still yields what the user actually copied.
+ Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, () =>
+ {
+ try
+ {
+ Clipboard.SetText(originalText);
+ }
+ catch (ExternalException)
+ {
+ // Best effort - leave the rewritten text on the clipboard if restoring fails.
+ }
+ });
}
}
diff --git a/Source/NETworkManager.Localization/Resources/StaticStrings.Designer.cs b/Source/NETworkManager.Localization/Resources/StaticStrings.Designer.cs
index 08636974d6..7c03a29d38 100644
--- a/Source/NETworkManager.Localization/Resources/StaticStrings.Designer.cs
+++ b/Source/NETworkManager.Localization/Resources/StaticStrings.Designer.cs
@@ -241,7 +241,7 @@ public static string ExampleHostnameOrIPAddress {
}
///
- /// Looks up a localized string similar to 192.168.178.0/24; 10.0.0.0 - 10.0.0.9; 10.0.[0-9,20].[1-2]; server-01.borntoberoot.net/24.
+ /// Looks up a localized string similar to 192.168.178.0/24; 10.8.0.0-100; 10.8.[0-9,254].1; server-01.borntoberoot.net/28.
///
public static string ExampleHostRange {
get {
diff --git a/Source/NETworkManager.Localization/Resources/StaticStrings.resx b/Source/NETworkManager.Localization/Resources/StaticStrings.resx
index 897bf115a8..b107040973 100644
--- a/Source/NETworkManager.Localization/Resources/StaticStrings.resx
+++ b/Source/NETworkManager.Localization/Resources/StaticStrings.resx
@@ -145,7 +145,7 @@
SERVER-01 or 10.0.0.10
- 192.168.178.0/24; 10.0.0.0 - 10.0.0.9; 10.0.[0-9,20].[1-2]; server-01.borntoberoot.net/24
+ 192.168.178.0/24; 10.8.0.0-100; 10.8.[0-9,254].1; server-01.borntoberoot.net/28
10.0.0.10
diff --git a/Website/docs/application/ip-scanner.md b/Website/docs/application/ip-scanner.md
index 930f248c29..6c46b528d4 100644
--- a/Website/docs/application/ip-scanner.md
+++ b/Website/docs/application/ip-scanner.md
@@ -25,6 +25,7 @@ With the **IP Scanner** you can scan for active devices based on the hostname or
| -------------------------------- | -------------------------------------------------------------------------------------------- |
| `10.0.0.1` | Single IP address (`10.0.0.1`) |
| `10.0.0.100 - 10.0.0.199` | All IP addresses in a given range (`10.0.0.100`, `10.0.0.101`, ..., `10.0.0.199`) |
+| `10.0.0.100-199` | All IP addresses in a given range, shorthand for the last octet (`10.0.0.100`, ..., `10.0.0.199`) |
| `10.0.0.0/23` | All IP addresses in a subnet (`10.0.0.0`, ..., `10.0.1.255`) |
| `10.0.0.0/255.255.254.0` | All IP addresses in a subnet (`10.0.0.0`, ..., `10.0.1.255`) |
| `10.0.[0-9,20].[1-2]` | Multiple IP addresses like (`10.0.0.1`, `10.0.0.2`, `10.0.1.1`, ...,`10.0.9.2`, `10.0.20.1`) |
@@ -36,9 +37,7 @@ With the **IP Scanner** you can scan for active devices based on the hostname or
Multiple inputs can be combined with a semicolon (`;`). A column pasted from Excel (one entry per line) is converted to the semicolon-separated form automatically.
-Example: `10.0.0.0/24; 10.0.[10-20]1`
-
-Shorthand ranges like `192.168.0.1-100` (192.168.0.1 to 192.168.0.100) are also supported.
+Example: `10.0.0.0/24; 10.0.[10-20].1; 10.0.0.100-199`
:::
diff --git a/Website/docs/application/ping-monitor.md b/Website/docs/application/ping-monitor.md
index d6aa1a8bb5..08f2cc4a4b 100644
--- a/Website/docs/application/ping-monitor.md
+++ b/Website/docs/application/ping-monitor.md
@@ -22,6 +22,7 @@ ICMP (Internet Control Message Protocol) is a network-layer protocol used to sen
| -------------------------------- | ------------------------------------------------------------------------------------------ |
| `10.0.0.1` | Single IP address (`10.0.0.1`) |
| `10.0.0.100 - 10.0.0.199` | All IP addresses in a given range (`10.0.0.100`, `10.0.0.101`, ..., `10.0.0.199`) |
+| `10.0.0.100-199` | All IP addresses in a given range, shorthand for the last octet (`10.0.0.100`, ..., `10.0.0.199`) |
| `10.0.0.0/23` | All IP addresses in a subnet (`10.0.0.0`, ..., `10.0.1.255`) |
| `10.0.0.0/255.255.254.0` | All IP addresses in a subnet (`10.0.0.0`, ..., `10.0.1.255`) |
| `10.0.[0-9,20].[1-2]` | Multiple IP addresses like (`10.0.0.1`, `10.0.0.2`, `10.0.1.1`, ...,`10.0.9.2`, `10.0.20.1`) |
@@ -33,9 +34,7 @@ ICMP (Internet Control Message Protocol) is a network-layer protocol used to sen
Multiple inputs can be combined with a semicolon (`;`). A column pasted from Excel (one entry per line) is converted to the semicolon-separated form automatically.
-Example: `10.0.0.0/24; 10.0.[10-20]1`
-
-Shorthand ranges like `192.168.0.1-100` (192.168.0.1 to 192.168.0.100) are also supported.
+Example: `10.0.0.0/24; 10.0.[10-20].1; 10.0.0.100-199`
:::
diff --git a/Website/docs/application/port-scanner.md b/Website/docs/application/port-scanner.md
index 5613179653..cdcbd94457 100644
--- a/Website/docs/application/port-scanner.md
+++ b/Website/docs/application/port-scanner.md
@@ -31,6 +31,7 @@ TCP (Transmission Control Protocol) is a connection-oriented transport-layer pro
| -------------------------------- | -------------------------------------------------------------------------------------------- |
| `10.0.0.1` | Single IP address (`10.0.0.1`) |
| `10.0.0.100 - 10.0.0.199` | All IP addresses in a given range (`10.0.0.100`, `10.0.0.101`, ..., `10.0.0.199`) |
+| `10.0.0.100-199` | All IP addresses in a given range, shorthand for the last octet (`10.0.0.100`, ..., `10.0.0.199`) |
| `10.0.0.0/23` | All IP addresses in a subnet (`10.0.0.0`, ..., `10.0.1.255`) |
| `10.0.0.0/255.255.254.0` | All IP addresses in a subnet (`10.0.0.0`, ..., `10.0.1.255`) |
| `10.0.[0-9,20].[1-2]` | Multiple IP addresses like (`10.0.0.1`, `10.0.0.2`, `10.0.1.1`, ...,`10.0.9.2`, `10.0.20.1`) |
@@ -47,9 +48,7 @@ TCP (Transmission Control Protocol) is a connection-oriented transport-layer pro
Multiple inputs can be combined with a semicolon (`;`). A column pasted from Excel (one entry per line) is converted to the semicolon-separated form automatically.
-Example: `10.0.0.0/24; 10.0.[10-20]1` or `1-1024; 8080; 8443`
-
-Shorthand ranges like `192.168.0.1-100` (192.168.0.1 to 192.168.0.100) are also supported.
+Example: `10.0.0.0/24; 10.0.[10-20].1; 10.0.0.100-199` or `1-1024; 8080; 8443`
:::
diff --git a/Website/docs/changelog/next-release.md b/Website/docs/changelog/next-release.md
index 11ddf144a0..13ea8a10a3 100644
--- a/Website/docs/changelog/next-release.md
+++ b/Website/docs/changelog/next-release.md
@@ -33,18 +33,23 @@ Release date: **xx.xx.2026**
- The collapsed/expanded state of profile groups (e.g. **linux-server**) is now remembered per profile file and shared across all tools, instead of resetting every time you switch tools or restart the application. [#3539](https://github.com/BornToBeRoot/NETworkManager/pull/3539)
-**IP Scanner, Port Scanner & Ping Monitor**
-
-- Host input fields now accept newline-separated hosts (one per line, e.g. pasted from Excel) in addition to the existing semicolon (`;`) separator. Shorthand IPv4 ranges like `192.168.0.1-100` are supported as well. [#3568](https://github.com/BornToBeRoot/NETworkManager/pull/3568)
-
**IP Scanner**
+- Host input now accepts newline-separated hosts (one per line, e.g. a column pasted from Excel), converted automatically to the semicolon-separated form. Thanks to [@dearmb](https://github.com/dearmb) [#3568](https://github.com/BornToBeRoot/NETworkManager/pull/3568)
+- Host input now supports shorthand IPv4 ranges like `192.168.0.1-100`, in addition to the existing `192.168.0.0-192.168.0.100` and `192.168.[0-100].1` range formats. [#3568](https://github.com/BornToBeRoot/NETworkManager/pull/3568)
- Added `135` (RPC) and `9100` (raw printing) to the default **Ports** list used to detect if a host is reachable. [#3564](https://github.com/BornToBeRoot/NETworkManager/pull/3564)
- Reduced the default **Max. concurrent port threads** from `5` to `4`. [#3564](https://github.com/BornToBeRoot/NETworkManager/pull/3564)
- Reduced the default **Max. concurrent host threads** from `256` to `64`, a more conservative default that puts less simultaneous load on the scanned network. [#3564](https://github.com/BornToBeRoot/NETworkManager/pull/3564)
+**Ping Monitor**
+
+- Host input now accepts newline-separated hosts (one per line, e.g. a column pasted from Excel), converted automatically to the semicolon-separated form. [#3568](https://github.com/BornToBeRoot/NETworkManager/pull/3568)
+- Host input now supports shorthand IPv4 ranges like `192.168.0.1-100`, in addition to the existing `192.168.0.0-192.168.0.100` and `192.168.[0-100].1` range formats. [#3568](https://github.com/BornToBeRoot/NETworkManager/pull/3568)
+
**Port Scanner**
+- Host and Ports input fields now accept newline-separated entries (one per line, e.g. a column pasted from Excel), converted automatically to the semicolon-separated form. [#3568](https://github.com/BornToBeRoot/NETworkManager/pull/3568)
+- Host input now supports shorthand IPv4 ranges like `192.168.0.1-100`, in addition to the existing `192.168.0.0-192.168.0.100` and `192.168.[0-100].1` range formats. [#3568](https://github.com/BornToBeRoot/NETworkManager/pull/3568)
- Reduced the default **Max. concurrent host threads** from `5` to `4`. [#3564](https://github.com/BornToBeRoot/NETworkManager/pull/3564)
- Reduced the default **Max. concurrent port threads** from `256` to `64`, a more conservative default that puts less simultaneous load on the scanned host. [#3564](https://github.com/BornToBeRoot/NETworkManager/pull/3564)
- Added a new **Well-known ports** (`1-1024`) default port profile. [#3564](https://github.com/BornToBeRoot/NETworkManager/pull/3564)
From 606513583e09723f2d0a3a90f27cdc68a01b42c6 Mon Sep 17 00:00:00 2001
From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com>
Date: Sat, 15 Aug 2026 02:17:23 +0200
Subject: [PATCH 6/6] Chore: Copilot feedback
---
.../ComboBoxPasteBehavior.cs | 58 +++++++++----------
.../Network/HostRangeHelper.cs | 58 +++++++++----------
2 files changed, 53 insertions(+), 63 deletions(-)
diff --git a/Source/NETworkManager.Controls/ComboBoxPasteBehavior.cs b/Source/NETworkManager.Controls/ComboBoxPasteBehavior.cs
index 87397e2f65..cdcd3a47cd 100644
--- a/Source/NETworkManager.Controls/ComboBoxPasteBehavior.cs
+++ b/Source/NETworkManager.Controls/ComboBoxPasteBehavior.cs
@@ -4,7 +4,6 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
-using System.Windows.Threading;
namespace NETworkManager.Controls;
@@ -14,10 +13,10 @@ namespace NETworkManager.Controls;
///
/// Without this the WPF TextBox inside the ComboBox ( is
/// false) silently drops everything after the first line when multiline text is pasted.
-/// The conversion happens in before the paste
-/// command actually runs, by rewriting the clipboard content to the semicolon-separated form.
-/// The original clipboard content is restored afterwards, and clipboard access is guarded since
-/// the Windows clipboard can throw when briefly locked by another process.
+/// The conversion is applied by reading the clipboard, replacing the editable text box's
+/// selection with the converted text, and marking as
+/// handled - the system clipboard itself is never modified, so other applications (or a
+/// subsequent paste elsewhere) are unaffected.
///
public static class ComboBoxPasteBehavior
{
@@ -52,14 +51,19 @@ private static void OnPreviewExecuted(object sender, ExecutedRoutedEventArgs e)
if (e.Command != ApplicationCommands.Paste)
return;
- string originalText;
+ // The command originates from the focused editable text box inside the ComboBox
+ // template, which is where the pasted text actually needs to be inserted.
+ if (e.OriginalSource is not TextBox textBox)
+ return;
+
+ string text;
try
{
if (!Clipboard.ContainsText())
return;
- originalText = Clipboard.GetText();
+ text = Clipboard.GetText();
}
catch (ExternalException)
{
@@ -67,37 +71,29 @@ private static void OnPreviewExecuted(object sender, ExecutedRoutedEventArgs e)
return;
}
- // Only rewrite when there is actual multiline content (e.g. a column pasted from Excel).
- if (!originalText.Contains('\n') && !originalText.Contains('\r'))
+ // Only intervene when there is actual multiline content (e.g. a column pasted from
+ // Excel). Otherwise let the default paste command handle it as usual.
+ if (!text.Contains('\n') && !text.Contains('\r'))
return;
- var converted = string.Join(";", originalText
+ var converted = string.Join(";", text
.Replace("\r\n", "\n")
.Replace('\r', '\n')
.Split('\n', StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Trim()));
- try
- {
- Clipboard.SetText(converted);
- }
- catch (ExternalException)
- {
- return;
- }
+ // Replace the current selection with the converted text ourselves and mark the command
+ // handled, instead of letting the default paste run - this avoids touching the system
+ // clipboard entirely (no risk of losing other clipboard formats or racing a paste
+ // elsewhere).
+ var selectionStart = textBox.SelectionStart;
+ var textBefore = textBox.Text[..selectionStart];
+ var textAfter = textBox.Text[(selectionStart + textBox.SelectionLength)..];
- // Restore the original clipboard content once the paste command has consumed the
- // rewritten text, so pasting elsewhere afterwards still yields what the user actually copied.
- Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, () =>
- {
- try
- {
- Clipboard.SetText(originalText);
- }
- catch (ExternalException)
- {
- // Best effort - leave the rewritten text on the clipboard if restoring fails.
- }
- });
+ textBox.Text = textBefore + converted + textAfter;
+ textBox.SelectionStart = selectionStart + converted.Length;
+ textBox.SelectionLength = 0;
+
+ e.Handled = true;
}
}
diff --git a/Source/NETworkManager.Models/Network/HostRangeHelper.cs b/Source/NETworkManager.Models/Network/HostRangeHelper.cs
index e2ddeffde1..be420e8bd2 100644
--- a/Source/NETworkManager.Models/Network/HostRangeHelper.cs
+++ b/Source/NETworkManager.Models/Network/HostRangeHelper.cs
@@ -31,6 +31,27 @@ public static IEnumerable CreateListFromInput(string hosts)
.ToArray();
}
+ ///
+ /// Adds every IPv4 address in the inclusive range [, ] to
+ /// . Iterates using the unsigned IPv4 value widened to so the
+ /// inclusive upper bound stays representable even for the last address in the 32-bit space
+ /// (255.255.255.255), which would otherwise overflow arithmetic.
+ ///
+ private static void AddIPv4RangeToBag(IPAddress start, IPAddress end,
+ ConcurrentBag<(IPAddress ipAddress, string hostname)> hostsBag, CancellationToken ct)
+ {
+ var from = (long)unchecked((uint)IPv4Address.ToInt32(start));
+ var to = (long)unchecked((uint)IPv4Address.ToInt32(end));
+
+ Parallel.For(from, to + 1, (i, state) =>
+ {
+ if (ct.IsCancellationRequested)
+ state.Break();
+
+ hostsBag.Add((IPv4Address.FromInt32(unchecked((int)i)), string.Empty));
+ });
+ }
+
public static async Task<(List<(IPAddress ipAddress, string hostname)> hosts, List hostnamesNotResolved)>
ResolveAsync(IEnumerable hosts, bool dnsResolveHostnamePreferIPv4, CancellationToken cancellationToken)
{
@@ -56,14 +77,7 @@ public static IEnumerable CreateListFromInput(string hosts)
case var _ when RegexHelper.IPv4AddressSubnetmaskRegex().IsMatch(host):
var network = IPNetwork2.Parse(host);
- Parallel.For(IPv4Address.ToInt32(network.Network), IPv4Address.ToInt32(network.Broadcast) + 1,
- (i, state) =>
- {
- if (ct.IsCancellationRequested)
- state.Break();
-
- hostsBag.Add((IPv4Address.FromInt32(i), string.Empty));
- });
+ AddIPv4RangeToBag(network.Network, network.Broadcast, hostsBag, ct);
break;
@@ -72,14 +86,8 @@ public static IEnumerable CreateListFromInput(string hosts)
var shortRange = host.Split('-');
var shortBase = shortRange[0][..shortRange[0].LastIndexOf('.')];
- Parallel.For(IPv4Address.ToInt32(IPAddress.Parse(shortRange[0])),
- IPv4Address.ToInt32(IPAddress.Parse($"{shortBase}.{shortRange[1]}")) + 1, (i, state) =>
- {
- if (ct.IsCancellationRequested)
- state.Break();
-
- hostsBag.Add((IPv4Address.FromInt32(i), string.Empty));
- });
+ AddIPv4RangeToBag(IPAddress.Parse(shortRange[0]),
+ IPAddress.Parse($"{shortBase}.{shortRange[1]}"), hostsBag, ct);
break;
@@ -87,14 +95,7 @@ public static IEnumerable CreateListFromInput(string hosts)
case var _ when RegexHelper.IPv4AddressRangeRegex().IsMatch(host):
var range = host.Split('-');
- Parallel.For(IPv4Address.ToInt32(IPAddress.Parse(range[0])),
- IPv4Address.ToInt32(IPAddress.Parse(range[1])) + 1, (i, state) =>
- {
- if (ct.IsCancellationRequested)
- state.Break();
-
- hostsBag.Add((IPv4Address.FromInt32(i), string.Empty));
- });
+ AddIPv4RangeToBag(IPAddress.Parse(range[0]), IPAddress.Parse(range[1]), hostsBag, ct);
break;
@@ -188,14 +189,7 @@ public static IEnumerable CreateListFromInput(string hosts)
network = IPNetwork2.Parse(
$"{dnsResultWithSubnet.Value}/{hostAndSubnet[1]}");
- Parallel.For(IPv4Address.ToInt32(network.Network),
- IPv4Address.ToInt32(network.Broadcast) + 1, (i, state) =>
- {
- if (ct.IsCancellationRequested)
- state.Break();
-
- hostsBag.Add((IPv4Address.FromInt32(i), string.Empty));
- });
+ AddIPv4RangeToBag(network.Network, network.Broadcast, hostsBag, ct);
}
else
{