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/4] 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/4] 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/4] 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/4] 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}">