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 {