diff --git a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
index 9bdb5486be..33bee494ed 100644
--- a/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
+++ b/Source/NETworkManager.Localization/Resources/Strings.Designer.cs
@@ -2052,6 +2052,24 @@ public static string CommandLineArguments {
}
}
+ ///
+ /// Looks up a localized string similar to The command "{0}" does not accept arguments!.
+ ///
+ public static string CommandXDoesNotAcceptArguments {
+ get {
+ return ResourceManager.GetString("CommandXDoesNotAcceptArguments", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The command "{0}" does not support profiles!.
+ ///
+ public static string CommandXDoesNotSupportProfiles {
+ get {
+ return ResourceManager.GetString("CommandXDoesNotSupportProfiles", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Comment.
///
@@ -4057,6 +4075,15 @@ public static string EndTime {
}
}
+ ///
+ /// Looks up a localized string similar to Enter a command!.
+ ///
+ public static string EnterACommand {
+ get {
+ return ResourceManager.GetString("EnterACommand", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Enter a domain to query whois....
///
@@ -4156,6 +4183,15 @@ public static string EnterSubnetsToCreateAWideSubnet {
}
}
+ ///
+ /// Looks up a localized string similar to Enter the name of a profile after "@"!.
+ ///
+ public static string EnterTheNameOfAProfileAfterAt {
+ get {
+ return ResourceManager.GetString("EnterTheNameOfAProfileAfterAt", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Enter a valid AS number (e.g., "AS15169")!.
///
@@ -9292,6 +9328,15 @@ public static string ProfileWithThisNameAlreadyExists {
}
}
+ ///
+ /// Looks up a localized string similar to Profile "{0}" not found!.
+ ///
+ public static string ProfileXNotFound {
+ get {
+ return ResourceManager.GetString("ProfileXNotFound", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Program.
///
@@ -9752,6 +9797,15 @@ public static string ReceivedLost {
}
}
+ ///
+ /// Looks up a localized string similar to Recent.
+ ///
+ public static string Recent {
+ get {
+ return ResourceManager.GetString("Recent", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Reconnect.
///
@@ -11367,6 +11421,15 @@ public static string RunBackgroundJobEveryXMinute {
}
}
+ ///
+ /// Looks up a localized string similar to Run command.
+ ///
+ public static string RunCommand {
+ get {
+ return ResourceManager.GetString("RunCommand", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Run command....
///
@@ -11884,6 +11947,15 @@ public static string SettingsResetExclamationMark {
}
}
+ ///
+ /// Looks up a localized string similar to Several profiles are named "{0}", select one!.
+ ///
+ public static string SeveralProfilesAreNamedXSelectOne {
+ get {
+ return ResourceManager.GetString("SeveralProfilesAreNamedXSelectOne", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Share Gateway credentials with remote computer.
///
@@ -13244,6 +13316,24 @@ public static string Unknown {
}
}
+ ///
+ /// Looks up a localized string similar to Unknown argument "{0}"!.
+ ///
+ public static string UnknownArgumentX {
+ get {
+ return ResourceManager.GetString("UnknownArgumentX", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Unknown command "{0}"!.
+ ///
+ public static string UnknownCommandX {
+ get {
+ return ResourceManager.GetString("UnknownCommandX", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Unkown error!.
///
diff --git a/Source/NETworkManager.Localization/Resources/Strings.resx b/Source/NETworkManager.Localization/Resources/Strings.resx
index 17a09901cd..667accf8ac 100644
--- a/Source/NETworkManager.Localization/Resources/Strings.resx
+++ b/Source/NETworkManager.Localization/Resources/Strings.resx
@@ -4857,4 +4857,34 @@ Cloudflare may log your IP address and network information. See Cloudflare's pri
List of public suffixes (e.g. "co.uk") from publicsuffix.org (MPL-2.0), which is used to get the registered domain of a hostname for RDAP queries.
+
+ Profile "{0}" not found!
+
+
+ Several profiles are named "{0}", select one!
+
+
+ Enter a command!
+
+
+ Unknown command "{0}"!
+
+
+ The command "{0}" does not accept arguments!
+
+
+ The command "{0}" does not support profiles!
+
+
+ Enter the name of a profile after "@"!
+
+
+ Unknown argument "{0}"!
+
+
+ Run command
+
+
+ Recent
+
\ No newline at end of file
diff --git a/Source/NETworkManager.Models/EventSystem/EventSystem.cs b/Source/NETworkManager.Models/EventSystem/EventSystem.cs
deleted file mode 100644
index a5f6617773..0000000000
--- a/Source/NETworkManager.Models/EventSystem/EventSystem.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-using System;
-
-namespace NETworkManager.Models.EventSystem;
-
-public static class EventSystem
-{
- // This will notify the main window, to change the view to another application and redirect some data (hostname, ip)
- public static event EventHandler OnRedirectDataToApplicationEvent;
-
- public static void RedirectToApplication(ApplicationName application, string data)
- {
- OnRedirectDataToApplicationEvent?.Invoke(null, new EventSystemRedirectArgs(application, data));
- }
-
- // This will notify the main window, to change the view to the settings...
- public static event EventHandler OnRedirectToSettingsEvent;
-
- public static void RedirectToSettings()
- {
- OnRedirectToSettingsEvent?.Invoke(null, EventArgs.Empty);
- }
-}
\ No newline at end of file
diff --git a/Source/NETworkManager.Models/EventSystem/EventSystemRedirectArgs.cs b/Source/NETworkManager.Models/EventSystem/EventSystemRedirectArgs.cs
deleted file mode 100644
index 42c47ece31..0000000000
--- a/Source/NETworkManager.Models/EventSystem/EventSystemRedirectArgs.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using System;
-
-namespace NETworkManager.Models.EventSystem;
-
-public class EventSystemRedirectArgs : EventArgs
-{
- public EventSystemRedirectArgs(ApplicationName application, string args)
- {
- Application = application;
- Args = args;
- }
-
- public ApplicationName Application { get; set; }
- public string Args { get; set; }
-}
\ No newline at end of file
diff --git a/Source/NETworkManager.Models/RunCommand/RunCommandArgument.cs b/Source/NETworkManager.Models/RunCommand/RunCommandArgument.cs
new file mode 100644
index 0000000000..0271cb442f
--- /dev/null
+++ b/Source/NETworkManager.Models/RunCommand/RunCommandArgument.cs
@@ -0,0 +1,12 @@
+namespace NETworkManager.Models.RunCommand;
+
+///
+/// Second argument of a run command after the target.
+///
+public enum RunCommandArgument
+{
+ None,
+ Ports,
+ DNSServer,
+ Broadcast
+}
diff --git a/Source/NETworkManager.Models/RunCommand/RunCommandDefinition.cs b/Source/NETworkManager.Models/RunCommand/RunCommandDefinition.cs
new file mode 100644
index 0000000000..3b79913499
--- /dev/null
+++ b/Source/NETworkManager.Models/RunCommand/RunCommandDefinition.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace NETworkManager.Models.RunCommand;
+
+///
+/// Command of the run command dialog (e.g. "dnslookup" with the alias "dns"), which opens an application or the
+/// settings.
+///
+public sealed class RunCommandDefinition
+{
+ ///
+ /// Whether the command opens an application or the settings.
+ ///
+ public RunCommandType Type { get; init; }
+
+ ///
+ /// Application to open ( for the settings).
+ ///
+ public ApplicationName Application { get; init; }
+
+ ///
+ /// Command (lowercase, e.g. "dnslookup").
+ ///
+ public string Command { get; init; }
+
+ ///
+ /// Short forms of the command (lowercase, e.g. "dns", "nslookup").
+ ///
+ public IReadOnlyList Aliases { get; init; } = [];
+
+ ///
+ /// The command accepts a target (e.g. host, IP range, query).
+ ///
+ public bool SupportsTarget { get; init; }
+
+ ///
+ /// The command accepts a profile ("@name") instead of a target.
+ ///
+ public bool SupportsProfile { get; init; }
+
+ ///
+ /// Second argument after the target (e.g. ports or DNS server).
+ ///
+ public RunCommandArgument Argument { get; init; }
+
+ ///
+ /// Type keywords that can follow the target (lowercase, e.g. "a", "aaaa" for the DNS lookup).
+ ///
+ public IReadOnlyList Types { get; init; } = [];
+
+ ///
+ /// Syntax of the arguments for the hint (e.g. "<hosts> [server] [type]"), empty if there are none.
+ ///
+ public string Syntax { get; init; } = string.Empty;
+
+ ///
+ /// Checks if the text is the command or one of its aliases (case-insensitive).
+ ///
+ /// Command text.
+ /// True if the text matches.
+ public bool Matches(string text)
+ {
+ return string.Equals(Command, text, StringComparison.OrdinalIgnoreCase) ||
+ Aliases.Any(x => string.Equals(x, text, StringComparison.OrdinalIgnoreCase));
+ }
+}
diff --git a/Source/NETworkManager.Models/RunCommand/RunCommandParseError.cs b/Source/NETworkManager.Models/RunCommand/RunCommandParseError.cs
new file mode 100644
index 0000000000..bac326a81b
--- /dev/null
+++ b/Source/NETworkManager.Models/RunCommand/RunCommandParseError.cs
@@ -0,0 +1,15 @@
+namespace NETworkManager.Models.RunCommand;
+
+///
+/// Errors of a run command input.
+///
+public enum RunCommandParseError
+{
+ None,
+ Empty,
+ UnknownCommand,
+ NoArguments,
+ NoProfiles,
+ MissingProfileName,
+ UnknownArgument
+}
diff --git a/Source/NETworkManager.Models/RunCommand/RunCommandParseResult.cs b/Source/NETworkManager.Models/RunCommand/RunCommandParseResult.cs
new file mode 100644
index 0000000000..898166f108
--- /dev/null
+++ b/Source/NETworkManager.Models/RunCommand/RunCommandParseResult.cs
@@ -0,0 +1,63 @@
+namespace NETworkManager.Models.RunCommand;
+
+///
+/// Result of .
+///
+public sealed class RunCommandParseResult
+{
+ ///
+ /// Command that matches the first word, or null if there is none.
+ ///
+ public RunCommandDefinition Command { get; init; }
+
+ ///
+ /// First word as typed.
+ ///
+ public string CommandText { get; init; } = string.Empty;
+
+ ///
+ /// Target (e.g. "10.0.0.1;10.0.0.2"), or null if there is none.
+ ///
+ public string Target { get; init; }
+
+ ///
+ /// Name of the profile ("@name"), or null if no profile is used. Empty if only "@" was typed.
+ ///
+ public string ProfileName { get; init; }
+
+ ///
+ /// Second argument (e.g. ports or DNS server), or null if there is none.
+ ///
+ public string Argument { get; init; }
+
+ ///
+ /// Type keyword in lowercase (e.g. "aaaa"), or null if there is none.
+ ///
+ public string Type { get; init; }
+
+ ///
+ /// Part of the input that is being typed.
+ ///
+ public RunCommandPosition Position { get; init; }
+
+ ///
+ /// Word that is being typed (empty after a space), used to filter the suggestions.
+ ///
+ public string CurrentWord { get; init; } = string.Empty;
+
+ ///
+ /// Input without the word that is being typed (e.g. "dns borntoberoot.net " for "dns borntoberoot.net aa"),
+ /// used to complete a suggestion.
+ ///
+ public string InputBeforeCurrentWord { get; init; } = string.Empty;
+
+ ///
+ /// Error, if the input can't be run.
+ ///
+ public RunCommandParseError Error { get; init; }
+
+ ///
+ /// Value the error refers to (e.g. the unknown command or argument).
+ ///
+ public string ErrorValue { get; init; }
+}
diff --git a/Source/NETworkManager.Models/RunCommand/RunCommandParser.cs b/Source/NETworkManager.Models/RunCommand/RunCommandParser.cs
new file mode 100644
index 0000000000..51787a4ff7
--- /dev/null
+++ b/Source/NETworkManager.Models/RunCommand/RunCommandParser.cs
@@ -0,0 +1,204 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+
+namespace NETworkManager.Models.RunCommand;
+
+///
+/// Parses the input of the run command dialog: "command target [argument] [type]" or "command @profile".
+///
+public static partial class RunCommandParser
+{
+ ///
+ /// A list separator with whitespace around it ("10.0.0.1; 10.0.0.2"), or a value in quotes, which is kept as it is.
+ ///
+ [GeneratedRegex("\"[^\"]*(?:\"|$)|\\s*;\\s*")]
+ private static partial Regex ListSeparatorOrQuoteRegex();
+
+ ///
+ /// A word: a value in quotes (can contain spaces, the closing quote can be missing while typing) or a value
+ /// without spaces.
+ ///
+ [GeneratedRegex("\"[^\"]*(?:\"|$)|[^\\s\"]+")]
+ private static partial Regex WordRegex();
+
+ ///
+ /// Parses the input.
+ /// - Lists use ";" as everywhere in the application. Whitespace around ";" is removed, so whitespace only
+ /// separates the arguments.
+ /// - The first word is the command (name or alias), the second word the target. "@name" (the rest of the input,
+ /// since profile names can contain spaces) is a profile instead of a target.
+ /// - A value with spaces is written in quotes (e.g. a DNS server "Google Public DNS").
+ /// - Further words are a type keyword of the command or its second argument (e.g. ports, DNS server), in any
+ /// order.
+ ///
+ /// Input of the dialog.
+ /// Available commands.
+ /// Parse result, see .
+ public static RunCommandParseResult Parse(string input, IEnumerable commands)
+ {
+ input = (input ?? string.Empty).TrimStart();
+
+ // Whitespace around ";" is removed (outside of quotes), so a list that is being typed ("10.0.0.1; ") is still
+ // the same word.
+ var normalized = ListSeparatorOrQuoteRegex().Replace(input, x => x.Value.StartsWith('"') ? x.Value : ";");
+
+ // Words as typed (with quotes, to rebuild the input) and their values (without quotes)
+ var tokens = WordRegex().Matches(normalized).Select(x => x.Value).ToArray();
+ var words = tokens.Select(Unquote).ToArray();
+
+ // Whitespace in an open quote (e.g. "Google ) is part of the word that is being typed.
+ var endsWithWhitespace = normalized.Length > 0 && char.IsWhiteSpace(normalized[^1]) &&
+ !(tokens.Length > 0 && IsOpenQuote(tokens[^1]));
+
+ if (words.Length == 0)
+ return new RunCommandParseResult { Position = RunCommandPosition.Command, Error = RunCommandParseError.Empty };
+
+ var commandText = words[0];
+ var command = commands.FirstOrDefault(x => x.Matches(commandText));
+
+ // The command is being typed.
+ if (command == null || words.Length == 1 && !endsWithWhitespace)
+ return new RunCommandParseResult
+ {
+ Command = command,
+ CommandText = commandText,
+ Position = RunCommandPosition.Command,
+ CurrentWord = commandText,
+ Error = command == null ? RunCommandParseError.UnknownCommand : RunCommandParseError.None,
+ ErrorValue = command == null ? commandText : null
+ };
+
+ var commandPrefix = commandText + " ";
+
+ // Only the command (e.g. "ping ").
+ if (words.Length == 1)
+ return new RunCommandParseResult
+ {
+ Command = command,
+ CommandText = commandText,
+ Position = RunCommandPosition.Target,
+ InputBeforeCurrentWord = commandPrefix
+ };
+
+ // Profile: the rest of the input after "@" is the name.
+ if (words[1].StartsWith('@'))
+ {
+ var rest = input[(input.IndexOf('@') + 1)..].Trim();
+ string profileName;
+ string textAfterName = null;
+
+ // Name in quotes (e.g. @"My Server"): text after the closing quote is not part of the name.
+ var closingQuote = rest.StartsWith('"') ? rest.IndexOf('"', 1) : -1;
+
+ if (closingQuote > 0)
+ {
+ profileName = rest[1..closingQuote].Trim();
+
+ var remainder = rest[(closingQuote + 1)..].Trim();
+
+ if (remainder.Length > 0)
+ textAfterName = remainder;
+ }
+ else
+ {
+ // Quotes around the whole target (e.g. "@My Server") or an open quote while typing are removed.
+ profileName = rest.Trim('"').Trim();
+ }
+
+ var error = !command.SupportsProfile
+ ? RunCommandParseError.NoProfiles
+ : profileName.Length == 0
+ ? RunCommandParseError.MissingProfileName
+ : textAfterName != null
+ ? RunCommandParseError.UnknownArgument
+ : RunCommandParseError.None;
+
+ return new RunCommandParseResult
+ {
+ Command = command,
+ CommandText = commandText,
+ ProfileName = profileName,
+ Position = RunCommandPosition.Target,
+ CurrentWord = "@" + profileName,
+ InputBeforeCurrentWord = commandPrefix,
+ Error = error,
+ ErrorValue = error switch
+ {
+ RunCommandParseError.NoProfiles => command.Command,
+ RunCommandParseError.UnknownArgument => textAfterName,
+ _ => null
+ }
+ };
+ }
+
+ if (!command.SupportsTarget)
+ return new RunCommandParseResult
+ {
+ Command = command,
+ CommandText = commandText,
+ Position = RunCommandPosition.Target,
+ CurrentWord = endsWithWhitespace ? string.Empty : words[^1],
+ InputBeforeCurrentWord = string.Join(" ", tokens[..^(endsWithWhitespace ? 0 : 1)]) + " ",
+ Error = RunCommandParseError.NoArguments,
+ ErrorValue = command.Command
+ };
+
+ // Further words: type keyword or second argument, in any order. A type keyword is never the second argument.
+ string type = null;
+ string argument = null;
+ string unknownArgument = null;
+
+ foreach (var word in words[2..])
+ {
+ var keyword = word.ToLowerInvariant();
+
+ if (command.Types.Contains(keyword))
+ {
+ if (type == null)
+ type = keyword;
+ else
+ unknownArgument ??= word;
+ }
+ else if (argument == null && command.Argument != RunCommandArgument.None)
+ {
+ argument = word;
+ }
+ else
+ {
+ unknownArgument ??= word;
+ }
+ }
+
+ var isTypingTarget = words.Length == 2 && !endsWithWhitespace;
+ var currentWord = endsWithWhitespace ? string.Empty : words[^1];
+
+ return new RunCommandParseResult
+ {
+ Command = command,
+ CommandText = commandText,
+ Target = words[1],
+ Argument = argument,
+ Type = type,
+ Position = isTypingTarget ? RunCommandPosition.Target : RunCommandPosition.Argument,
+ CurrentWord = currentWord,
+ InputBeforeCurrentWord = string.Join(" ", tokens[..^(endsWithWhitespace ? 0 : 1)]) + " ",
+ Error = unknownArgument == null ? RunCommandParseError.None : RunCommandParseError.UnknownArgument,
+ ErrorValue = unknownArgument
+ };
+ }
+
+ ///
+ /// Removes the quotes of a value (e.g. "Google Public DNS").
+ ///
+ private static string Unquote(string word)
+ {
+ return word.StartsWith('"') ? word.Trim('"') : word;
+ }
+
+ private static bool IsOpenQuote(string token)
+ {
+ return token.StartsWith('"') && (token.Length == 1 || !token.EndsWith('"'));
+ }
+}
diff --git a/Source/NETworkManager.Models/RunCommand/RunCommandPosition.cs b/Source/NETworkManager.Models/RunCommand/RunCommandPosition.cs
new file mode 100644
index 0000000000..66efa88f6b
--- /dev/null
+++ b/Source/NETworkManager.Models/RunCommand/RunCommandPosition.cs
@@ -0,0 +1,11 @@
+namespace NETworkManager.Models.RunCommand;
+
+///
+/// Part of the input that is being typed (used for the suggestions).
+///
+public enum RunCommandPosition
+{
+ Command,
+ Target,
+ Argument
+}
diff --git a/Source/NETworkManager.Models/RunCommand/RunCommandType.cs b/Source/NETworkManager.Models/RunCommand/RunCommandType.cs
new file mode 100644
index 0000000000..6f10bbbc56
--- /dev/null
+++ b/Source/NETworkManager.Models/RunCommand/RunCommandType.cs
@@ -0,0 +1,10 @@
+namespace NETworkManager.Models.RunCommand;
+
+///
+/// What a run command opens.
+///
+public enum RunCommandType
+{
+ Application,
+ Setting
+}
diff --git a/Source/NETworkManager.Profiles/ProfileInfo.cs b/Source/NETworkManager.Profiles/ProfileInfo.cs
index 05f61c63bb..ebbe5562f2 100644
--- a/Source/NETworkManager.Profiles/ProfileInfo.cs
+++ b/Source/NETworkManager.Profiles/ProfileInfo.cs
@@ -1,5 +1,6 @@
//using System.Collections.Generic;
using NETworkManager.Controls;
+using NETworkManager.Models;
using NETworkManager.Models.Network;
using NETworkManager.Models.PowerShell;
using NETworkManager.Models.PuTTY;
@@ -504,4 +505,61 @@ public ProfileInfo(ProfileInfo profile)
public bool IPGeolocation_Enabled { get; set; }
public bool IPGeolocation_InheritHost { get; set; } = true;
public string IPGeolocation_Host { get; set; }
+
+ ///
+ /// Checks if the profile is enabled for an application.
+ ///
+ /// Application to check.
+ /// True if the profile is enabled for the application.
+ public bool IsEnabled(ApplicationName application)
+ {
+ return application switch
+ {
+ ApplicationName.NetworkInterface => NetworkInterface_Enabled,
+ ApplicationName.IPScanner => IPScanner_Enabled,
+ ApplicationName.PortScanner => PortScanner_Enabled,
+ ApplicationName.PingMonitor => PingMonitor_Enabled,
+ ApplicationName.Traceroute => Traceroute_Enabled,
+ ApplicationName.DNSLookup => DNSLookup_Enabled,
+ ApplicationName.RemoteDesktop => RemoteDesktop_Enabled,
+ ApplicationName.PowerShell => PowerShell_Enabled,
+ ApplicationName.PuTTY => PuTTY_Enabled,
+ ApplicationName.TigerVNC => TigerVNC_Enabled,
+ ApplicationName.WebConsole => WebConsole_Enabled,
+ ApplicationName.SNMP => SNMP_Enabled,
+ ApplicationName.WakeOnLAN => WakeOnLAN_Enabled,
+ ApplicationName.RDAP => RDAP_Enabled,
+ ApplicationName.Whois => Whois_Enabled,
+ ApplicationName.IPGeolocation => IPGeolocation_Enabled,
+ _ => false
+ };
+ }
+
+ ///
+ /// Gets the target of the profile in an application, e.g. the host, IP range, domain, URL or MAC address.
+ ///
+ /// Application to get the target for.
+ /// Target, or null if the application has none.
+ public string GetTarget(ApplicationName application)
+ {
+ return application switch
+ {
+ ApplicationName.IPScanner => IPScanner_HostOrIPRange,
+ ApplicationName.PortScanner => PortScanner_Host,
+ ApplicationName.PingMonitor => PingMonitor_Host,
+ ApplicationName.Traceroute => Traceroute_Host,
+ ApplicationName.DNSLookup => DNSLookup_Host,
+ ApplicationName.RemoteDesktop => RemoteDesktop_Host,
+ ApplicationName.PowerShell => PowerShell_Host,
+ ApplicationName.PuTTY => PuTTY_HostOrSerialLine,
+ ApplicationName.TigerVNC => TigerVNC_Host,
+ ApplicationName.WebConsole => WebConsole_Url,
+ ApplicationName.SNMP => SNMP_Host,
+ ApplicationName.WakeOnLAN => WakeOnLAN_MACAddress,
+ ApplicationName.RDAP => RDAP_Query,
+ ApplicationName.Whois => Whois_Domain,
+ ApplicationName.IPGeolocation => IPGeolocation_Host,
+ _ => null
+ };
+ }
}
\ No newline at end of file
diff --git a/Source/NETworkManager.Settings/SettingsInfo.cs b/Source/NETworkManager.Settings/SettingsInfo.cs
index 2338dddb2e..3281631f50 100644
--- a/Source/NETworkManager.Settings/SettingsInfo.cs
+++ b/Source/NETworkManager.Settings/SettingsInfo.cs
@@ -808,6 +808,22 @@ public bool ExpandApplicationView
}
}
+ // Run command
+
+ public ObservableCollection RunCommand_History
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+
+ UpdateCollectionChanged(field, value);
+ field = value;
+ OnPropertyChanged();
+ }
+ } = [];
+
#endregion
#region Dashboard
@@ -4056,6 +4072,9 @@ public SettingsInfo()
// General
General_ApplicationList.CollectionChanged += CollectionChanged;
+ // Run command
+ RunCommand_History.CollectionChanged += CollectionChanged;
+
// Network
Network_CustomDNSServers.CollectionChanged += CollectionChanged;
diff --git a/Source/NETworkManager.Tests/NETworkManager.Tests.csproj b/Source/NETworkManager.Tests/NETworkManager.Tests.csproj
new file mode 100644
index 0000000000..233c58941c
--- /dev/null
+++ b/Source/NETworkManager.Tests/NETworkManager.Tests.csproj
@@ -0,0 +1,18 @@
+
+
+ Exe
+ NETworkManager.Tests
+ NETworkManager.Tests
+ net10.0-windows10.0.22621.0
+ false
+ true
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/NETworkManager.Tests/RunCommand/RunCommandParserTests.cs b/Source/NETworkManager.Tests/RunCommand/RunCommandParserTests.cs
new file mode 100644
index 0000000000..dccb2af8f9
--- /dev/null
+++ b/Source/NETworkManager.Tests/RunCommand/RunCommandParserTests.cs
@@ -0,0 +1,317 @@
+using NETworkManager.Models;
+using NETworkManager.Models.RunCommand;
+using Xunit;
+
+namespace NETworkManager.Tests.RunCommand;
+
+public class RunCommandParserTests
+{
+ private static readonly RunCommandDefinition[] Commands =
+ [
+ new()
+ {
+ Type = RunCommandType.Application, Application = ApplicationName.PingMonitor, Command = "pingmonitor",
+ Aliases = ["ping"], SupportsTarget = true, SupportsProfile = true
+ },
+ new()
+ {
+ Type = RunCommandType.Application, Application = ApplicationName.RemoteDesktop, Command = "remotedesktop",
+ Aliases = ["rdp"], SupportsTarget = true, SupportsProfile = true
+ },
+ new()
+ {
+ Type = RunCommandType.Application, Application = ApplicationName.DNSLookup, Command = "dnslookup",
+ Aliases = ["dns", "nslookup"], SupportsTarget = true, SupportsProfile = true,
+ Argument = RunCommandArgument.DNSServer, Types = ["a", "aaaa", "cname", "ptr"]
+ },
+ new()
+ {
+ Type = RunCommandType.Application, Application = ApplicationName.PortScanner, Command = "portscanner",
+ Aliases = ["portscan"], SupportsTarget = true, SupportsProfile = true, Argument = RunCommandArgument.Ports
+ },
+ new()
+ {
+ Type = RunCommandType.Application, Application = ApplicationName.SubnetCalculator,
+ Command = "subnetcalculator"
+ },
+ new() { Type = RunCommandType.Setting, Command = "settings" }
+ ];
+
+ private static RunCommandParseResult Parse(string input)
+ {
+ return RunCommandParser.Parse(input, Commands);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData(" ")]
+ [InlineData(null)]
+ public void Empty(string input)
+ {
+ var result = Parse(input);
+
+ Assert.Equal(RunCommandParseError.Empty, result.Error);
+ Assert.Equal(RunCommandPosition.Command, result.Position);
+ }
+
+ [Theory]
+ [InlineData("ping", "pingmonitor")]
+ [InlineData("PING", "pingmonitor")]
+ [InlineData("nslookup", "dnslookup")]
+ [InlineData("dnslookup", "dnslookup")]
+ public void CommandAndAliases(string input, string command)
+ {
+ var result = Parse(input);
+
+ Assert.Equal(command, result.Command.Command);
+ Assert.Equal(RunCommandParseError.None, result.Error);
+ Assert.Equal(RunCommandPosition.Command, result.Position);
+ Assert.Equal(input, result.CurrentWord);
+ }
+
+ [Fact]
+ public void UnknownCommand()
+ {
+ var result = Parse("pin 10.0.0.1");
+
+ Assert.Null(result.Command);
+ Assert.Equal(RunCommandParseError.UnknownCommand, result.Error);
+ Assert.Equal("pin", result.ErrorValue);
+ }
+
+ [Fact]
+ public void CommandWithoutTarget()
+ {
+ var result = Parse("ping ");
+
+ Assert.Equal(RunCommandParseError.None, result.Error);
+ Assert.Null(result.Target);
+ Assert.Equal(RunCommandPosition.Target, result.Position);
+ Assert.Equal("ping ", result.InputBeforeCurrentWord);
+ }
+
+ [Fact]
+ public void Target()
+ {
+ var result = Parse("ping 10.0.0.1");
+
+ Assert.Equal("10.0.0.1", result.Target);
+ Assert.Equal(RunCommandPosition.Target, result.Position);
+ Assert.Equal("10.0.0.1", result.CurrentWord);
+ Assert.Equal("ping ", result.InputBeforeCurrentWord);
+ }
+
+ [Theory]
+ [InlineData("ping 10.0.0.1; 10.0.0.2")]
+ [InlineData("ping 10.0.0.1 ;10.0.0.2")]
+ [InlineData("ping 10.0.0.1 ; 10.0.0.2")]
+ public void ListWithWhitespace(string input)
+ {
+ var result = Parse(input);
+
+ Assert.Equal("10.0.0.1;10.0.0.2", result.Target);
+ Assert.Equal(RunCommandParseError.None, result.Error);
+ }
+
+ [Theory]
+ [InlineData("dns borntoberoot.net 1.1.1.1 cname")]
+ [InlineData("dns borntoberoot.net CNAME 1.1.1.1")]
+ public void DNSServerAndTypeInAnyOrder(string input)
+ {
+ var result = Parse(input);
+
+ Assert.Equal("borntoberoot.net", result.Target);
+ Assert.Equal("1.1.1.1", result.Argument);
+ Assert.Equal("cname", result.Type);
+ Assert.Equal(RunCommandParseError.None, result.Error);
+ }
+
+ [Theory]
+ [InlineData("dns borntoberoot.net \"Google Public DNS\" aaaa")]
+ [InlineData("dns borntoberoot.net aaaa \"Google Public DNS\"")]
+ public void QuotedArgument(string input)
+ {
+ var result = Parse(input);
+
+ Assert.Equal("Google Public DNS", result.Argument);
+ Assert.Equal("aaaa", result.Type);
+ Assert.Equal(RunCommandParseError.None, result.Error);
+ }
+
+ [Fact]
+ public void OpenQuoteIsTheWordThatIsBeingTyped()
+ {
+ // Whitespace in an open quote doesn't start a new word.
+ var result = Parse("dns borntoberoot.net \"Google ");
+
+ Assert.Equal(RunCommandPosition.Argument, result.Position);
+ Assert.Equal("Google ", result.CurrentWord);
+ Assert.Equal("dns borntoberoot.net ", result.InputBeforeCurrentWord);
+ }
+
+ [Fact]
+ public void CompletionKeepsQuotes()
+ {
+ var result = Parse("dns borntoberoot.net \"Google Public DNS\" aa");
+
+ Assert.Equal("aa", result.CurrentWord);
+ Assert.Equal("dns borntoberoot.net \"Google Public DNS\" ", result.InputBeforeCurrentWord);
+ }
+
+ [Theory]
+ [InlineData("rdp @\"Web Server 01\"")]
+ [InlineData("rdp \"@Web Server 01\"")]
+ public void QuotedProfileName(string input)
+ {
+ var result = Parse(input);
+
+ Assert.Equal("Web Server 01", result.ProfileName);
+ }
+
+ [Fact]
+ public void TextAfterAQuotedProfileName()
+ {
+ var result = Parse("rdp @\"Web Server\" x");
+
+ Assert.Equal("Web Server", result.ProfileName);
+ Assert.Equal(RunCommandParseError.UnknownArgument, result.Error);
+ Assert.Equal("x", result.ErrorValue);
+ }
+
+ [Fact]
+ public void ListThatIsBeingTyped()
+ {
+ // "; " is the separator of a list that is still being typed, not the end of the target.
+ var result = Parse("ping 10.0.0.1; ");
+
+ Assert.Equal(RunCommandPosition.Target, result.Position);
+ Assert.Equal("10.0.0.1;", result.Target);
+ Assert.Equal("ping ", result.InputBeforeCurrentWord);
+ }
+
+ [Fact]
+ public void SemicolonInQuotesIsKept()
+ {
+ var result = Parse("dns borntoberoot.net \"DNS ; Server\"");
+
+ Assert.Equal("DNS ; Server", result.Argument);
+ }
+
+ [Fact]
+ public void HostsAndType()
+ {
+ var result = Parse("dns 1.1.1.1; 1.0.0.1 ptr");
+
+ Assert.Equal("1.1.1.1;1.0.0.1", result.Target);
+ Assert.Equal("ptr", result.Type);
+ Assert.Null(result.Argument);
+ }
+
+ [Fact]
+ public void PortsWithWhitespace()
+ {
+ var result = Parse("portscan 10.0.0.1; 10.0.0.2 22; 80; 443");
+
+ Assert.Equal("10.0.0.1;10.0.0.2", result.Target);
+ Assert.Equal("22;80;443", result.Argument);
+ Assert.Equal(RunCommandParseError.None, result.Error);
+ }
+
+ [Fact]
+ public void TargetThatLooksLikeAType()
+ {
+ // The second word is always the target.
+ var result = Parse("dns a");
+
+ Assert.Equal("a", result.Target);
+ Assert.Null(result.Type);
+ }
+
+ [Theory]
+ [InlineData("ping 10.0.0.1 foo", "foo")]
+ [InlineData("dns borntoberoot.net 1.1.1.1 8.8.8.8", "8.8.8.8")]
+ [InlineData("dns borntoberoot.net a aaaa 1.1.1.1", "aaaa")]
+ public void UnknownArgument(string input, string unknown)
+ {
+ var result = Parse(input);
+
+ Assert.Equal(RunCommandParseError.UnknownArgument, result.Error);
+ Assert.Equal(unknown, result.ErrorValue);
+ }
+
+ [Theory]
+ [InlineData("subnetcalculator 10.0.0.0/24")]
+ [InlineData("settings general")]
+ public void NoArguments(string input)
+ {
+ var result = Parse(input);
+
+ Assert.Equal(RunCommandParseError.NoArguments, result.Error);
+ }
+
+ [Fact]
+ public void NoArgumentsCurrentWordIsTheLastWord()
+ {
+ var result = Parse("settings foo ba");
+
+ Assert.Equal("ba", result.CurrentWord);
+ Assert.Equal("settings foo ", result.InputBeforeCurrentWord);
+ }
+
+ [Theory]
+ [InlineData("ping @dns-01", "dns-01")]
+ [InlineData("ping @Web Server 01", "Web Server 01")]
+ [InlineData("ping @Web Server 01 ", "Web Server 01")]
+ [InlineData("ping @a;b", "a;b")]
+ public void Profile(string input, string profileName)
+ {
+ var result = Parse(input);
+
+ Assert.Equal(profileName, result.ProfileName);
+ Assert.Null(result.Target);
+ Assert.Equal(RunCommandParseError.None, result.Error);
+ Assert.Equal(RunCommandPosition.Target, result.Position);
+ }
+
+ [Fact]
+ public void ProfileWithoutName()
+ {
+ var result = Parse("ping @");
+
+ Assert.Equal(string.Empty, result.ProfileName);
+ Assert.Equal(RunCommandParseError.MissingProfileName, result.Error);
+ Assert.Equal("@", result.CurrentWord);
+ }
+
+ [Fact]
+ public void ProfileNotSupported()
+ {
+ var result = Parse("settings @dns-01");
+
+ Assert.Equal(RunCommandParseError.NoProfiles, result.Error);
+ }
+
+ [Fact]
+ public void HostWithAtSign()
+ {
+ // "@" is only a profile at the start of the target (e.g. "ssh admin@server01").
+ var result = Parse("ping admin@server01");
+
+ Assert.Equal("admin@server01", result.Target);
+ Assert.Null(result.ProfileName);
+ }
+
+ [Theory]
+ [InlineData("dns borntoberoot.net ", RunCommandPosition.Argument, "", "dns borntoberoot.net ")]
+ [InlineData("dns borntoberoot.net aa", RunCommandPosition.Argument, "aa", "dns borntoberoot.net ")]
+ [InlineData("dns borntoberoot.net 1.1.1.1 cn", RunCommandPosition.Argument, "cn", "dns borntoberoot.net 1.1.1.1 ")]
+ [InlineData("dns border", RunCommandPosition.Target, "border", "dns ")]
+ public void PositionForSuggestions(string input, RunCommandPosition position, string currentWord, string before)
+ {
+ var result = Parse(input);
+
+ Assert.Equal(position, result.Position);
+ Assert.Equal(currentWord, result.CurrentWord);
+ Assert.Equal(before, result.InputBeforeCurrentWord);
+ }
+}
diff --git a/Source/NETworkManager.Tests/Utilities/ListHelperTests.cs b/Source/NETworkManager.Tests/Utilities/ListHelperTests.cs
new file mode 100644
index 0000000000..924f9a1aad
--- /dev/null
+++ b/Source/NETworkManager.Tests/Utilities/ListHelperTests.cs
@@ -0,0 +1,125 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using NETworkManager.Utilities;
+using Xunit;
+
+namespace NETworkManager.Tests.Utilities;
+
+public class ListHelperTests
+{
+ [Fact]
+ public void AddsNewEntryAtTheTop()
+ {
+ ObservableCollection list = ["b", "c"];
+
+ ListHelper.Modify(list, "a", 5);
+
+ Assert.Equal(["a", "b", "c"], list);
+ }
+
+ [Fact]
+ public void MovesExistingEntryToTheTop()
+ {
+ ObservableCollection list = ["a", "b", "c"];
+
+ ListHelper.Modify(list, "c", 5);
+
+ Assert.Equal(["c", "a", "b"], list);
+ }
+
+ [Fact]
+ public void RemovesOldestEntriesBeyondTheLength()
+ {
+ ObservableCollection list = ["b", "c", "d"];
+
+ ListHelper.Modify(list, "a", 3);
+
+ Assert.Equal(["a", "b", "c"], list);
+ }
+
+ [Fact]
+ public void ShortensAListThatIsTooLong()
+ {
+ // E.g. after the history length was reduced in the settings.
+ ObservableCollection list = ["a", "b", "c", "d"];
+
+ ListHelper.Modify(list, "a", 2);
+
+ Assert.Equal(["a", "b"], list);
+ }
+
+ [Fact]
+ public void RemovesDuplicatesOfTheEntry()
+ {
+ ObservableCollection list = [1, 2, 3, 2];
+
+ ListHelper.Modify(list, 2, 10);
+
+ Assert.Equal([2, 1, 3], list);
+ }
+
+ [Fact]
+ public void DoesNotChangeTheListIfTheEntryIsAlreadyTheNewest()
+ {
+ ObservableCollection list = ["a", "b"];
+ var changes = 0;
+ list.CollectionChanged += (_, _) => changes++;
+
+ ListHelper.Modify(list, "a", 5);
+
+ Assert.Equal(0, changes);
+ }
+
+ [Fact]
+ public void MovesInsteadOfResetting()
+ {
+ // A reset (Clear) would remove the entry that a ComboBox shows, which can clear its text.
+ ObservableCollection list = ["a", "b", "c"];
+ var actions = new System.Collections.Generic.List();
+ list.CollectionChanged += (_, e) => actions.Add(e.Action);
+
+ ListHelper.Modify(list, "b", 5);
+
+ Assert.Equal([NotifyCollectionChangedAction.Move], actions);
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ public void IgnoresEmptyEntries(string entry)
+ {
+ ObservableCollection list = ["a"];
+
+ ListHelper.Modify(list, entry, 5);
+
+ Assert.Equal(["a"], list);
+ }
+
+ [Fact]
+ public void IgnoresDefaultValue()
+ {
+ // E.g. a port of 0 if the port is not used.
+ ObservableCollection list = [22];
+
+ ListHelper.Modify(list, 0, 5);
+
+ Assert.Equal([22], list);
+ }
+
+ [Fact]
+ public void UsesTheComparerAndKeepsTheLatestSpelling()
+ {
+ ObservableCollection list = ["a", "ping 10.0.0.1"];
+
+ ListHelper.Modify(list, "Ping 10.0.0.1", 5, StringComparer.OrdinalIgnoreCase);
+
+ Assert.Equal(["Ping 10.0.0.1", "a"], list);
+ }
+
+ [Fact]
+ public void ThrowsIfTheLengthIsNotPositive()
+ {
+ Assert.Throws(() => ListHelper.Modify(new ObservableCollection(), "a", 0));
+ }
+}
diff --git a/Source/NETworkManager.Utilities.WPF/FrameworkElementExtensions.cs b/Source/NETworkManager.Utilities.WPF/FrameworkElementExtensions.cs
new file mode 100644
index 0000000000..ccd28ae6f0
--- /dev/null
+++ b/Source/NETworkManager.Utilities.WPF/FrameworkElementExtensions.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace NETworkManager.Utilities.WPF;
+
+///
+/// Extension methods for .
+///
+public static class FrameworkElementExtensions
+{
+ ///
+ /// Default time to wait for the event. A view that is shown is loaded
+ /// within milliseconds.
+ ///
+ private static readonly TimeSpan DefaultLoadedTimeout = TimeSpan.FromSeconds(5);
+
+ ///
+ /// Waits until the element is loaded (e.g. before showing a dialog from a view that was just created, which
+ /// MahApps doesn't allow before the view is loaded). Completes immediately if the element is already loaded.
+ /// Stops waiting after the timeout (e.g. the user switched to another view before it was loaded), so an action
+ /// doesn't run unexpectedly when the view is shown later.
+ ///
+ /// Element to wait for.
+ /// Maximum time to wait. Default: 5 seconds.
+ /// True if the element is loaded, false if the timeout elapsed.
+ public static async Task WaitForLoadedAsync(this FrameworkElement element, TimeSpan? timeout = null)
+ {
+ if (element.IsLoaded)
+ return true;
+
+ var taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ void OnLoaded(object sender, RoutedEventArgs e)
+ {
+ taskCompletionSource.TrySetResult();
+ }
+
+ element.Loaded += OnLoaded;
+
+ try
+ {
+ var completedTask = await Task.WhenAny(taskCompletionSource.Task,
+ Task.Delay(timeout ?? DefaultLoadedTimeout));
+
+ return completedTask == taskCompletionSource.Task;
+ }
+ finally
+ {
+ element.Loaded -= OnLoaded;
+ }
+ }
+}
diff --git a/Source/NETworkManager.Utilities/ListHelper.cs b/Source/NETworkManager.Utilities/ListHelper.cs
index 93815c0c3c..e1336b997c 100644
--- a/Source/NETworkManager.Utilities/ListHelper.cs
+++ b/Source/NETworkManager.Utilities/ListHelper.cs
@@ -1,38 +1,70 @@
using System;
using System.Collections.Generic;
+using System.Collections.ObjectModel;
namespace NETworkManager.Utilities;
///
-/// Helper class for modifying lists by adding new entries and removing old ones if the list exceeds a specified length.
+/// Helper class for history lists: add an entry as the newest one and remove the oldest ones if the list exceeds a
+/// specified length.
///
public static class ListHelper
{
///
- /// Modify a list by adding the and removing the oldest entry if the list is full.
- /// If an entry or multiple ones already exist in the list, they will be removed before adding the new entry.
+ /// Adds the as the newest (first) entry of the list. An existing entry is moved to the top
+ /// (duplicates are removed) and the oldest entries are removed if the list exceeds the .
+ /// The list is changed in place, so controls bound to it (e.g. the history of a ComboBox) keep their text and are
+ /// notified only about the actual changes. Default values (null, 0) and empty strings are not added (e.g. optional
+ /// inputs).
///
- /// List to modify. Used with to add and remove entries.
+ /// List to modify (e.g. a history of the settings).
/// Entry to add to the list.
/// Maximum length of the list. Oldest entries will be removed if the list exceeds this length.
+ /// Comparer for existing entries (e.g. case-insensitive). If null, the default comparer is used.
/// Type of the list entries. Currently or .
- /// Modified list with the new entry added and oldest entries removed if necessary.
- public static List Modify(List list, T entry, int length)
+ public static void Modify(ObservableCollection list, T entry, int length,
+ IEqualityComparer comparer = null)
{
- int index;
+ comparer ??= EqualityComparer.Default;
- while ((index = list.IndexOf(entry)) != -1)
+ if (length <= 0)
+ throw new ArgumentOutOfRangeException(nameof(length), "Length must be greater than zero.");
+
+ if (EqualityComparer.Default.Equals(entry, default) || entry is string text && string.IsNullOrEmpty(text))
+ return;
+
+ var index = -1;
+
+ for (var i = 0; i < list.Count; i++)
{
- list.RemoveAt(index);
+ if (!comparer.Equals(list[i], entry))
+ continue;
+
+ index = i;
+ break;
}
- if (length <= 0)
- throw new ArgumentOutOfRangeException(nameof(length), "Length must be greater than zero.");
+ if (index < 0)
+ {
+ list.Insert(0, entry);
+ }
+ else
+ {
+ if (index > 0)
+ list.Move(index, 0);
- while (list.Count >= length)
- list.RemoveAt(list.Count - 1);
+ // Keep the latest spelling (e.g. with a case-insensitive comparer)
+ if (!EqualityComparer.Default.Equals(list[0], entry))
+ list[0] = entry;
+ }
- list.Insert(0, entry);
+ // Remove duplicates (e.g. from older settings files)
+ for (var i = list.Count - 1; i > 0; i--)
+ {
+ if (comparer.Equals(list[i], entry))
+ list.RemoveAt(i);
+ }
- return list;
+ while (list.Count > length)
+ list.RemoveAt(list.Count - 1);
}
}
diff --git a/Source/NETworkManager.sln b/Source/NETworkManager.sln
index bffc498bae..feaef76599 100644
--- a/Source/NETworkManager.sln
+++ b/Source/NETworkManager.sln
@@ -43,6 +43,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dragablz", "3rdparty\Dragab
EndProject
Project("{B7DD6F7E-DEF8-4E67-B5B7-07EF123DB6F0}") = "NETworkManager.Setup", "NETworkManager.Setup\NETworkManager.Setup.wixproj", "{B474758D-E657-4DA6-AB2B-63764C5FBAF1}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NETworkManager.Tests", "NETworkManager.Tests\NETworkManager.Tests.csproj", "{53C05A40-D2B3-42F1-AA1D-E220BBF9E426}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -338,6 +340,26 @@ Global
{B474758D-E657-4DA6-AB2B-63764C5FBAF1}.Release|x64.Build.0 = Release|x64
{B474758D-E657-4DA6-AB2B-63764C5FBAF1}.Release|x86.ActiveCfg = Release|x86
{B474758D-E657-4DA6-AB2B-63764C5FBAF1}.Release|x86.Build.0 = Release|x86
+ {53C05A40-D2B3-42F1-AA1D-E220BBF9E426}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {53C05A40-D2B3-42F1-AA1D-E220BBF9E426}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {53C05A40-D2B3-42F1-AA1D-E220BBF9E426}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {53C05A40-D2B3-42F1-AA1D-E220BBF9E426}.Debug|ARM.Build.0 = Debug|Any CPU
+ {53C05A40-D2B3-42F1-AA1D-E220BBF9E426}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {53C05A40-D2B3-42F1-AA1D-E220BBF9E426}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {53C05A40-D2B3-42F1-AA1D-E220BBF9E426}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {53C05A40-D2B3-42F1-AA1D-E220BBF9E426}.Debug|x64.Build.0 = Debug|Any CPU
+ {53C05A40-D2B3-42F1-AA1D-E220BBF9E426}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {53C05A40-D2B3-42F1-AA1D-E220BBF9E426}.Debug|x86.Build.0 = Debug|Any CPU
+ {53C05A40-D2B3-42F1-AA1D-E220BBF9E426}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {53C05A40-D2B3-42F1-AA1D-E220BBF9E426}.Release|Any CPU.Build.0 = Release|Any CPU
+ {53C05A40-D2B3-42F1-AA1D-E220BBF9E426}.Release|ARM.ActiveCfg = Release|Any CPU
+ {53C05A40-D2B3-42F1-AA1D-E220BBF9E426}.Release|ARM.Build.0 = Release|Any CPU
+ {53C05A40-D2B3-42F1-AA1D-E220BBF9E426}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {53C05A40-D2B3-42F1-AA1D-E220BBF9E426}.Release|ARM64.Build.0 = Release|Any CPU
+ {53C05A40-D2B3-42F1-AA1D-E220BBF9E426}.Release|x64.ActiveCfg = Release|Any CPU
+ {53C05A40-D2B3-42F1-AA1D-E220BBF9E426}.Release|x64.Build.0 = Release|Any CPU
+ {53C05A40-D2B3-42F1-AA1D-E220BBF9E426}.Release|x86.ActiveCfg = Release|Any CPU
+ {53C05A40-D2B3-42F1-AA1D-E220BBF9E426}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Source/NETworkManager/ApplicationRequest.cs b/Source/NETworkManager/ApplicationRequest.cs
new file mode 100644
index 0000000000..d9728eaf11
--- /dev/null
+++ b/Source/NETworkManager/ApplicationRequest.cs
@@ -0,0 +1,74 @@
+using DnsClient;
+using NETworkManager.Models;
+using NETworkManager.Models.RDAP;
+using NETworkManager.Profiles;
+
+namespace NETworkManager;
+
+///
+/// Request to open an application (tool), optionally with a target or a profile, e.g. from the run command or from
+/// another tool ("Open in ..."). Applications that support more options have a derived request.
+///
+/// Application to open.
+public record ApplicationRequest(ApplicationName Application)
+{
+ ///
+ /// Target of the application, e.g. host(s), IP range, query or URL. If null (and no is set),
+ /// the application is only opened.
+ ///
+ public string Target { get; init; }
+
+ ///
+ /// Profile to run in the application with its settings. is ignored if set.
+ ///
+ public ProfileInfo Profile { get; init; }
+}
+
+///
+/// Request to open the DNS Lookup.
+///
+public sealed record DNSLookupRequest() : ApplicationRequest(ApplicationName.DNSLookup)
+{
+ ///
+ /// DNS server(s) to use (e.g. "1.1.1.1"). If null, the DNS server selected in the application is used.
+ ///
+ public string DNSServer { get; init; }
+
+ ///
+ /// Query type. If null, the query type selected in the application is used.
+ ///
+ public QueryType? QueryType { get; init; }
+}
+
+///
+/// Request to open the Port Scanner.
+///
+public sealed record PortScannerRequest() : ApplicationRequest(ApplicationName.PortScanner)
+{
+ ///
+ /// Ports to scan (e.g. "22;80;443" or "1-1024"). If null, the ports of the application are used.
+ ///
+ public string Ports { get; init; }
+}
+
+///
+/// Request to open Wake on LAN.
+///
+public sealed record WakeOnLANRequest() : ApplicationRequest(ApplicationName.WakeOnLAN)
+{
+ ///
+ /// Broadcast address. If null, the broadcast address of the application is used.
+ ///
+ public string Broadcast { get; init; }
+}
+
+///
+/// Request to open RDAP.
+///
+public sealed record RDAPRequest() : ApplicationRequest(ApplicationName.RDAP)
+{
+ ///
+ /// Query type. If null, the type is detected from the query ().
+ ///
+ public RDAPQueryType? QueryType { get; init; }
+}
diff --git a/Source/NETworkManager/Controls/RunCommandComboBox.cs b/Source/NETworkManager/Controls/RunCommandComboBox.cs
deleted file mode 100644
index df6489c31c..0000000000
--- a/Source/NETworkManager/Controls/RunCommandComboBox.cs
+++ /dev/null
@@ -1,337 +0,0 @@
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.Windows.Controls;
-using System.Windows.Controls.Primitives;
-using System.Windows.Data;
-using System.Windows.Input;
-
-namespace NETworkManager.Controls;
-
-/*
- * Modified from https://stackoverflow.com/a/58066259
- */
-
-public class RunCommandComboBox : ComboBox
-{
- private readonly UserChange _isDropDownOpenUc;
- private string _currentFilter = string.Empty;
- private bool _textBoxFreezed;
-
- public RunCommandComboBox()
- {
- _isDropDownOpenUc = new UserChange(v => IsDropDownOpen = v);
- DropDownOpened += FilteredComboBox_DropDownOpened;
-
- IsEditable = true;
- IsTextSearchEnabled = false;
- StaysOpenOnEdit = true;
- IsReadOnly = false;
-
- Loaded += (_, _) =>
- {
- if (EditableTextBox != null)
- new TextBoxBaseUserChangeTracker(EditableTextBox).UserTextChanged += FilteredComboBox_UserTextChange;
- };
- }
-
- private TextBox EditableTextBox => GetTemplateChild("PART_EditableTextBox") as TextBox;
-
- ///
- /// Open the dropdown when the control gets focus.
- ///
- /// Focus change information.
- protected override void OnPreviewGotKeyboardFocus(KeyboardFocusChangedEventArgs e)
- {
- base.OnPreviewGotKeyboardFocus(e);
-
- Width = 350;
- IsDropDownOpen = true;
- }
-
- protected override void OnPreviewLostKeyboardFocus(KeyboardFocusChangedEventArgs e)
- {
- base.OnPreviewLostKeyboardFocus(e);
-
- Width = 250;
- }
-
- protected override void OnPreviewKeyDown(KeyEventArgs e)
- {
- switch (e.Key)
- {
- case Key.Down when !IsDropDownOpen:
- IsDropDownOpen = true;
- e.Handled = true;
- break;
- case Key.Escape:
- ClearFilter();
- Text = string.Empty;
- IsDropDownOpen = true;
- e.Handled = true;
- break;
- // Auto-complete the text when the user presses the enter key, tab key, or right arrow
- // and the drop-down is open.
- case Key.Enter:
- case Key.Tab:
- case Key.Right:
- if (IsDropDownOpen)
- {
- // Insert the selected item into the text box.
- var result = InsertSelectedItem();
-
- // Close the drop-down.
- IsDropDownOpen = false;
-
- // If the enter key was pressed and the item has no arguments then use the default
- // behavior (which will fire the enter key command in the control - to redirect
- // to another application).
- if (e.Key == Key.Enter && result == 0)
- {
- e.Handled = false;
- break;
- }
-
- // Handle the key press (tab, enter or right arrow) to auto-complete the text and
- // don't pass it to the control.
- if (result != -1)
- e.Handled = true;
-
- // If no item was selected, the result will be -1 and the key press will be
- // passed to the control.
- }
-
- break;
- }
-
- base.OnPreviewKeyDown(e);
- }
-
- ///
- /// Inserts the selected item into the text box.
- ///
- ///
- /// Returns 0 if the item was inserted, 1 if the item was inserted with example arguments, and -1 if the item was
- /// not inserted.
- ///
- private int InsertSelectedItem()
- {
- if (SelectedItem is not RunCommandInfo info)
- return -1;
-
- Text = $"{info.Command}";
-
- if (info.CanHandleArguments)
- {
- Text += $" {info.ExampleArguments}";
- EditableTextBox.SelectionStart = Text.Length - info.ExampleArguments.Length;
- EditableTextBox.SelectionLength = info.ExampleArguments.Length;
-
- return 1;
- }
-
- EditableTextBox.SelectionStart = Text.Length;
- EditableTextBox.SelectionLength = 0;
-
- return 0;
- }
-
- private void ClearFilter()
- {
- if (string.IsNullOrEmpty(_currentFilter))
- return;
-
- _currentFilter = string.Empty;
-
- CollectionViewSource.GetDefaultView(ItemsSource).Refresh();
- }
-
- private void FilteredComboBox_DropDownOpened(object sender, EventArgs e)
- {
- if (_isDropDownOpenUc.IsUserChange)
- ClearFilter();
- }
-
- private void FilteredComboBox_UserTextChange(object sender, EventArgs e)
- {
- if (_textBoxFreezed)
- return;
-
- var tb = EditableTextBox;
- _currentFilter = tb.SelectionStart + tb.SelectionLength == tb.Text.Length
- ? tb.Text[..tb.SelectionStart].ToLower()
- : tb.Text.ToLower();
-
- RefreshFilter();
- }
-
- protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
- {
- if (newValue != null)
- {
- var view = CollectionViewSource.GetDefaultView(newValue);
- view.Filter += FilterItem;
- }
-
- if (oldValue != null)
- {
- var view = CollectionViewSource.GetDefaultView(oldValue);
- if (view != null) view.Filter -= FilterItem;
- }
-
- base.OnItemsSourceChanged(oldValue, newValue);
- }
-
- private void RefreshFilter()
- {
- if (ItemsSource == null)
- return;
-
- var view = CollectionViewSource.GetDefaultView(ItemsSource);
-
- FreezeTextBoxState(() =>
- {
- var isDropDownOpen = IsDropDownOpen;
- //always hide because showing it enables the user to pick with up and down keys, otherwise it's not working because of the glitch in view.Refresh()
- _isDropDownOpenUc.Set(false);
- view.Refresh();
-
- if (!string.IsNullOrEmpty(_currentFilter) || isDropDownOpen)
- _isDropDownOpenUc.Set(true);
-
- if (SelectedItem != null)
- return;
-
- // Loop through all items
- foreach (RunCommandInfo item in ItemsSource)
- {
- // Check if any item contains the text
- if (!item.TranslatedName.Contains(Text, StringComparison.OrdinalIgnoreCase) &&
- !item.Name.Contains(Text, StringComparison.OrdinalIgnoreCase) &&
- !item.Command.Contains(Text, StringComparison.OrdinalIgnoreCase))
- continue;
-
- SelectedItem = item;
- break;
- }
- });
- }
-
- private void FreezeTextBoxState(Action action)
- {
- _textBoxFreezed = true;
- var tb = EditableTextBox;
- var text = Text;
- var selStart = tb.SelectionStart;
- var selLen = tb.SelectionLength;
- action();
- Text = text;
- tb.SelectionStart = selStart;
- tb.SelectionLength = selLen;
- _textBoxFreezed = false;
- }
-
- private bool FilterItem(object value)
- {
- if (value is not RunCommandInfo info)
- return false;
-
- return _currentFilter.Length == 0 ||
- info.TranslatedName.Contains(Text, StringComparison.OrdinalIgnoreCase) ||
- info.Name.Contains(Text, StringComparison.OrdinalIgnoreCase) ||
- info.Command.Contains(Text, StringComparison.OrdinalIgnoreCase);
-
- //value.ToString()!.ToLower().Contains(_currentFilter);
- }
-
- private class TextBoxBaseUserChangeTracker
- {
- private readonly List _pressedKeys = new();
-
- public TextBoxBaseUserChangeTracker(TextBoxBase textBoxBase)
- {
- TextBoxBase = textBoxBase;
- var lastText = TextBoxBase.ToString();
-
- textBoxBase.PreviewTextInput += (_, _) => { IsTextInput = true; };
-
- textBoxBase.TextChanged += (_, e) =>
- {
- var isUserChange = _pressedKeys.Count > 0 || IsTextInput || lastText == TextBoxBase.ToString();
- IsTextInput = false;
- lastText = TextBoxBase.ToString();
- if (isUserChange)
- UserTextChanged?.Invoke(this, e);
- };
-
- textBoxBase.PreviewKeyDown += (_, e) =>
- {
- switch (e.Key)
- {
- case Key.Back:
- case Key.Space:
- if (!_pressedKeys.Contains(e.Key))
- _pressedKeys.Add(e.Key);
- break;
- }
-
- if (e.Key != Key.Back)
- return;
-
- var textBox = textBoxBase as TextBox;
- if (textBox!.SelectionStart <= 0 || textBox.SelectionLength <= 0 ||
- textBox.SelectionStart + textBox.SelectionLength != textBox.Text.Length)
- return;
-
- textBox.SelectionStart--;
- textBox.SelectionLength++;
-
- e.Handled = true;
-
- UserTextChanged?.Invoke(this, e);
- };
-
- textBoxBase.PreviewKeyUp += (_, e) =>
- {
- if (_pressedKeys.Contains(e.Key))
- _pressedKeys.Remove(e.Key);
- };
-
- textBoxBase.LostFocus += (_, _) =>
- {
- _pressedKeys.Clear();
- IsTextInput = false;
- };
- }
-
- private bool IsTextInput { get; set; }
-
- private TextBoxBase TextBoxBase { get; }
- public event EventHandler UserTextChanged;
- }
-
- private class UserChange
- {
- private readonly Action _action;
-
- public UserChange(Action action)
- {
- _action = action;
- }
-
- public bool IsUserChange { get; private set; } = true;
-
- public void Set(T val)
- {
- try
- {
- IsUserChange = false;
- _action(val);
- }
- finally
- {
- IsUserChange = true;
- }
- }
- }
-}
\ No newline at end of file
diff --git a/Source/NETworkManager/IApplicationRequestHandler.cs b/Source/NETworkManager/IApplicationRequestHandler.cs
new file mode 100644
index 0000000000..aad22b5fc9
--- /dev/null
+++ b/Source/NETworkManager/IApplicationRequestHandler.cs
@@ -0,0 +1,14 @@
+namespace NETworkManager;
+
+///
+/// Application view that can handle an with a target or a profile (e.g. open a new
+/// tab and start a scan). Views without this interface are only opened.
+///
+public interface IApplicationRequestHandler
+{
+ ///
+ /// Handles the request. Called after the application view is shown.
+ ///
+ /// Request with a target or a profile.
+ void Open(ApplicationRequest request);
+}
diff --git a/Source/NETworkManager/MainWindow.xaml b/Source/NETworkManager/MainWindow.xaml
index 2fb168ba6e..a41009ad5a 100644
--- a/Source/NETworkManager/MainWindow.xaml
+++ b/Source/NETworkManager/MainWindow.xaml
@@ -90,7 +90,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -640,6 +533,7 @@
Style="{StaticResource FadeOutScrollViewer}">
x.IsDefault) ??
applicationList.FirstOrDefault(x => x.IsVisible);
- // Scroll into view
- if (SelectedApplication != null)
- ListViewApplication.ScrollIntoView(SelectedApplication);
-
// Expand application view
ApplicationViewIsExpanded = SettingsManager.Current.ExpandApplicationView;
}
@@ -1044,96 +1023,54 @@ private void ClearSearchOnApplicationListMinimize()
ApplicationSearch = string.Empty;
- // Scroll into view
+ // Scroll into view. The selection doesn't change here, so ListViewApplication_OnSelectionChanged doesn't scroll.
ListViewApplication.ScrollIntoView(SelectedApplication);
}
- private async void EventSystem_RedirectDataToApplicationEvent(object sender, EventArgs e)
+ ///
+ /// Scrolls the selected application into view, also if it was selected from somewhere else (e.g. the dashboard,
+ /// another application or the run command).
+ ///
+ private void ListViewApplication_OnSelectionChanged(object sender,
+ System.Windows.Controls.SelectionChangedEventArgs e)
{
- if (e is not EventSystemRedirectArgs data)
- return;
+ if (ListViewApplication.SelectedItem != null)
+ ListViewApplication.ScrollIntoView(ListViewApplication.SelectedItem);
+ }
- // Try to find the application
- var application = Applications.Cast().FirstOrDefault(x => x.Name == data.Application);
+ private async void NavigationManager_ApplicationRequested(object sender, ApplicationRequest request)
+ {
+ // Clear the search, otherwise the application may be filtered out
+ ApplicationSearch = string.Empty;
+
+ // Try to find the application (hidden applications can't be opened)
+ var application = Applications.Cast().FirstOrDefault(x => x.Name == request.Application);
// Show error message if the application was not found
if (application == null)
{
await DialogHelper.ShowMessageAsync(Application.Current.MainWindow, Strings.Error,
string.Format(Strings.CouldNotFindApplicationXXXMessage,
- $"{data.Application}"), ChildWindowIcon.Error);
+ $"{request.Application}"), ChildWindowIcon.Error);
return;
}
+ if (SettingsViewIsOpen)
+ CloseSettings();
+
// Change application view
SelectedApplication = application;
- if (string.IsNullOrEmpty(data.Args))
+ if (string.IsNullOrEmpty(request.Target) && request.Profile == null)
return;
- // Crate a new tab / perform action
- switch (data.Application)
- {
- case ApplicationName.Dashboard:
- case ApplicationName.NetworkInterface:
- case ApplicationName.WiFi:
- break;
- case ApplicationName.IPScanner:
- _ipScannerHostView.AddTab(data.Args);
- break;
- case ApplicationName.PortScanner:
- _portScannerHostView.AddTab(data.Args);
- break;
- case ApplicationName.PingMonitor:
- _pingMonitorHostView.AddHost(data.Args);
- break;
- case ApplicationName.Traceroute:
- _tracerouteHostView.AddTab(data.Args);
- break;
- case ApplicationName.DNSLookup:
- _dnsLookupHostView.AddTab(data.Args);
- break;
- case ApplicationName.RemoteDesktop:
- _remoteDesktopHostView.AddTab(data.Args);
- break;
- case ApplicationName.PowerShell:
- _powerShellHostView.AddTab(data.Args);
- break;
- case ApplicationName.PuTTY:
- _puttyHostView.AddTab(data.Args);
- break;
- case ApplicationName.TigerVNC:
- _tigerVNCHostView.AddTab(data.Args);
- break;
- case ApplicationName.WebConsole:
- break;
- case ApplicationName.SNMP:
- _snmpHostView.AddTab(data.Args);
- break;
- case ApplicationName.RDAP:
- _rdapHostView.AddTab(data.Args);
- break;
- case ApplicationName.Whois:
- _whoisHostView.AddTab(data.Args);
- break;
- case ApplicationName.SNTPLookup:
- case ApplicationName.HostsFileEditor:
- case ApplicationName.Firewall:
- case ApplicationName.DiscoveryProtocol:
- case ApplicationName.WakeOnLAN:
- case ApplicationName.IPGeolocation:
- case ApplicationName.SubnetCalculator:
- case ApplicationName.BitCalculator:
- case ApplicationName.Lookup:
- case ApplicationName.Connections:
- case ApplicationName.Listeners:
- case ApplicationName.NeighborTable:
- break;
- default:
- Log.Error($"Cannot redirect data to unknown application: {data.Application}");
- break;
- }
+ // Create a new tab / perform the action in the application (only if it is shown)
+ if (SelectedApplication?.Name == request.Application &&
+ ContentControlApplication.Content is IApplicationRequestHandler handler)
+ handler.Open(request);
+ else
+ Log.Warn($"Application {request.Application} cannot handle a request with a target or profile.");
}
///
@@ -1154,178 +1091,45 @@ private void TextBoxApplicationSearch_OnPreviewExecuted(object sender, ExecutedR
#region Run Command
- #region Variables
-
- public ICollectionView RunCommands
- {
- get;
- private set
- {
- if (value == field)
- return;
-
- field = value;
- OnPropertyChanged();
- }
- }
-
- public RunCommandInfo SelectedRunCommand
- {
- get;
- set
- {
- if (value == field)
- return;
-
- field = value;
- OnPropertyChanged();
- }
- }
-
- public string RunCommandSearch
- {
- get;
- set
- {
- if (value == field)
- return;
-
- field = value;
-
- RefreshRunCommandsView();
-
- OnPropertyChanged();
- }
- }
-
- #endregion
-
- #region ICommands & Actions
-
public ICommand OpenRunCommand => new RelayCommand(_ => OpenRunAction());
private void OpenRunAction()
{
- ConfigurationManager.OnDialogOpen();
-
- FlyoutRunCommandIsOpen = true;
- }
-
- public ICommand RunCommandDoCommand => new RelayCommand(_ => RunCommandDoAction());
-
- private void RunCommandDoAction()
- {
- RunCommandDo();
- }
-
- public ICommand RunCommandCloseCommand => new RelayCommand(_ => RunCommandCloseAction());
-
- private void RunCommandCloseAction()
- {
- RunCommandFlyoutClose();
- }
-
- #endregion
-
- #region Methods
-
- private void SetRunCommandsView(RunCommandInfo selectedRunCommand = null)
- {
- RunCommands = new CollectionViewSource { Source = RunCommandManager.GetList() }.View;
-
- RunCommands.Filter = o =>
- {
- if (o is not RunCommandInfo info)
- return false;
-
- if (string.IsNullOrEmpty(RunCommandSearch))
- return true;
-
- return info.TranslatedName.IndexOf(RunCommandSearch, StringComparison.OrdinalIgnoreCase) > -1 ||
- info.Name.IndexOf(RunCommandSearch, StringComparison.OrdinalIgnoreCase) > -1;
- };
-
- if (selectedRunCommand != null)
- SelectedRunCommand = RunCommands.Cast()
- .FirstOrDefault(x => x.Name == selectedRunCommand.Name);
-
- SelectedRunCommand ??= RunCommands.Cast().FirstOrDefault();
- }
-
- private void RefreshRunCommandsView()
- {
- SetRunCommandsView(SelectedRunCommand);
- }
-
- ///
- /// Execute the selected run command.
- ///
- private void RunCommandDo()
- {
- // Do nothing if no command is selected
- if (SelectedRunCommand == null)
+ // Only one dialog at a time (e.g. the shortcut while another dialog is open)
+ if (ConfigurationManager.Current.IsChildWindowOpen)
return;
- // Do the command
- switch (SelectedRunCommand.Type)
+ var childWindow = new RunCommandChildWindow();
+
+ void CloseDialog()
{
- // Redirect to application
- case RunCommandType.Application:
- if (SettingsViewIsOpen)
- CloseSettings();
+ childWindow.IsOpen = false;
+ ConfigurationManager.Current.IsChildWindowOpen = false;
- var applicationName = (ApplicationName)Enum.Parse(typeof(ApplicationName), SelectedRunCommand.Name);
- EventSystem.RedirectToApplication(applicationName, string.Empty);
- break;
- // Redirect to settings
- case RunCommandType.Setting:
- EventSystem.RedirectToSettings();
- break;
+ ConfigurationManager.OnDialogClose();
}
- // Close the flyout
- RunCommandFlyoutClose(true);
- }
-
- ///
- /// Close the run command flyout and clear the search.
- ///
- private void RunCommandFlyoutClose(bool clearSearch = false)
- {
- if (!FlyoutRunCommandIsOpen)
- return;
-
- FlyoutRunCommandIsOpen = false;
-
- ConfigurationManager.OnDialogClose();
-
- // Clear the search
- if (clearSearch)
- RunCommandSearch = string.Empty;
- }
+ var childWindowViewModel = new RunCommandViewModel(instance =>
+ {
+ CloseDialog();
- #endregion
+ if (instance.IsSettings)
+ NavigationManager.OpenSettings();
+ else
+ NavigationManager.Open(instance.Request);
+ }, _ => CloseDialog());
- #region Events
+ childWindow.DataContext = childWindowViewModel;
- private void ListViewRunCommand_OnPreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
- {
- RunCommandDo();
- }
+ ConfigurationManager.Current.IsChildWindowOpen = true;
- private void FlyoutRunCommand_IsKeyboardFocusWithinChanged(object sender, DependencyPropertyChangedEventArgs e)
- {
- // Close flyout if the focus is lost.
- if (e.NewValue is not false)
- return;
+ ConfigurationManager.OnDialogOpen();
- RunCommandFlyoutClose();
+ _ = this.ShowChildWindowAsync(childWindow);
}
#endregion
- #endregion
-
#region Settings
private void OpenSettings()
@@ -1348,8 +1152,15 @@ private void OpenSettings()
SettingsViewIsOpen = true;
}
- private void EventSystem_RedirectToSettingsEvent(object sender, EventArgs e)
+ private void NavigationManager_SettingsRequested(object sender, EventArgs e)
{
+ // The settings are already open (e.g. run command "settings"), keep the current page.
+ if (SettingsViewIsOpen)
+ return;
+
+ // Clear the search, otherwise no application may be selected (the settings page depends on it)
+ ApplicationSearch = string.Empty;
+
OpenSettings();
}
diff --git a/Source/NETworkManager/NavigationManager.cs b/Source/NETworkManager/NavigationManager.cs
new file mode 100644
index 0000000000..1212629d41
--- /dev/null
+++ b/Source/NETworkManager/NavigationManager.cs
@@ -0,0 +1,36 @@
+using System;
+
+namespace NETworkManager;
+
+///
+/// Navigation between the applications (tools) and the settings. The main window handles the requests.
+///
+public static class NavigationManager
+{
+ ///
+ /// Occurs when an application should be opened, see .
+ ///
+ public static event EventHandler ApplicationRequested;
+
+ ///
+ /// Occurs when the settings should be opened, see .
+ ///
+ public static event EventHandler SettingsRequested;
+
+ ///
+ /// Opens an application and passes the request (target, profile, options) to it.
+ ///
+ /// Request with the application to open.
+ public static void Open(ApplicationRequest request)
+ {
+ ApplicationRequested?.Invoke(null, request);
+ }
+
+ ///
+ /// Opens the settings of the current application.
+ ///
+ public static void OpenSettings()
+ {
+ SettingsRequested?.Invoke(null, EventArgs.Empty);
+ }
+}
diff --git a/Source/NETworkManager/RunCommandInfo.cs b/Source/NETworkManager/RunCommandInfo.cs
deleted file mode 100644
index 4a86602f5b..0000000000
--- a/Source/NETworkManager/RunCommandInfo.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-namespace NETworkManager;
-
-public class RunCommandInfo
-{
- public RunCommandType Type { get; init; }
-
- ///
- /// Name.
- ///
- public string Name { get; init; }
-
- ///
- /// Translated name.
- ///
- public string TranslatedName { get; init; }
-
- ///
- /// Command to run.
- ///
- public string Command { get; set; }
-
- ///
- /// Indicates whether (optional) arguments can be passed.
- ///
- public bool CanHandleArguments { get; init; }
-
- ///
- /// Example arguments.
- ///
- public string ExampleArguments { get; set; }
-
- public override string ToString()
- {
- return $"{Command}";
- }
-}
\ No newline at end of file
diff --git a/Source/NETworkManager/RunCommandManager.cs b/Source/NETworkManager/RunCommandManager.cs
index 8308c96170..9bb85f8466 100644
--- a/Source/NETworkManager/RunCommandManager.cs
+++ b/Source/NETworkManager/RunCommandManager.cs
@@ -1,82 +1,349 @@
-using System.Collections.Generic;
-using System.Linq;
+using DnsClient;
using NETworkManager.Localization;
using NETworkManager.Localization.Resources;
using NETworkManager.Models;
+using NETworkManager.Models.Network;
+using NETworkManager.Models.RDAP;
+using NETworkManager.Models.RunCommand;
+using NETworkManager.Profiles;
+using NETworkManager.Settings;
+using NETworkManager.Validators;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Net;
+using System.Windows.Controls;
namespace NETworkManager;
+///
+/// Commands of the run command dialog: definitions, profiles, validation and the request to run.
+///
public static class RunCommandManager
{
- private const string DefaultArguments = "";
+ ///
+ /// Type keywords of RDAP and their query type.
+ ///
+ private static readonly Dictionary RDAPTypes = new()
+ {
+ ["domain"] = RDAPQueryType.Domain,
+ ["nameserver"] = RDAPQueryType.Nameserver,
+ ["tld"] = RDAPQueryType.TLD,
+ ["ip"] = RDAPQueryType.IPAddress,
+ ["asn"] = RDAPQueryType.ASN,
+ ["entity"] = RDAPQueryType.Entity
+ };
- public static IEnumerable GetList()
+ ///
+ /// Gets the commands of the visible applications and the settings.
+ ///
+ /// Available commands.
+ public static IReadOnlyList GetCommands()
{
- return GetApplicationList().Concat(GetSettingsList());
+ var visibleApplications = SettingsManager.Current.General_ApplicationList.Where(x => x.IsVisible)
+ .Select(x => x.Name).ToHashSet();
+
+ // Created on each call, since the syntax is translated.
+ return [.. CreateCommands().Where(x => x.Type == RunCommandType.Setting || visibleApplications.Contains(x.Application))];
}
- private static IEnumerable GetSettingsList()
+ ///
+ /// Gets the names of the DNS servers configured in the DNS lookup (e.g. "Cloudflare").
+ ///
+ /// Names of the DNS servers.
+ public static IEnumerable GetDNSServers()
{
- return
- [
- new RunCommandInfo
- {
- Type = RunCommandType.Setting,
- Name = "Settings",
- TranslatedName = Strings.Settings,
- Command = "settings",
- CanHandleArguments = false,
- ExampleArguments = string.Empty
- }
- ];
+ return SettingsManager.Current.DNSLookup_DNSServers.Select(x => x.Name);
+ }
+
+ ///
+ /// Gets the translated name of the application (or settings) of a command.
+ ///
+ /// Command.
+ /// Translated name.
+ public static string GetTranslatedName(RunCommandDefinition command)
+ {
+ return command.Type == RunCommandType.Setting
+ ? Strings.Settings
+ : ResourceTranslator.Translate(ResourceIdentifier.ApplicationName, command.Application);
}
- private static IEnumerable GetApplicationList()
+ ///
+ /// Gets the profiles that are enabled for the application of a command.
+ ///
+ /// Command.
+ /// Profiles, ordered by group and name.
+ public static IEnumerable GetProfiles(RunCommandDefinition command)
{
- return ApplicationManager.GetNames().Where(name => name != ApplicationName.None).Select(name =>
- new RunCommandInfo
+ if (!command.SupportsProfile || ProfileManager.LoadedProfileFileData == null)
+ return [];
+
+ return ProfileManager.LoadedProfileFileData.Groups.SelectMany(x => x.Profiles)
+ .Where(x => x.IsEnabled(command.Application))
+ .OrderBy(x => x.Group).ThenBy(x => x.Name);
+ }
+
+ ///
+ /// Validates the input and creates the request to run.
+ ///
+ /// Parsed input.
+ ///
+ /// Profile selected in the suggestions. If null, the profile is looked up by its name ("@name"), which must be
+ /// unique.
+ ///
+ /// Request to open the application, or null for the settings or on error.
+ /// Error message, or null if the input is valid.
+ public static string TryCreateRequest(RunCommandParseResult result, ProfileInfo profile,
+ out ApplicationRequest request)
+ {
+ request = null;
+
+ var error = GetParseErrorMessage(result);
+
+ if (error != null)
+ return error;
+
+ if (result.Command.Type == RunCommandType.Setting)
+ return null;
+
+ // Profile ("@name" or selected in the suggestions)
+ if (result.ProfileName != null)
+ {
+ if (profile == null)
{
- Type = RunCommandType.Application,
- Name = name.ToString(),
- TranslatedName = ResourceTranslator.Translate(ResourceIdentifier.ApplicationName, name),
- Command = name switch
- {
- _ => name.ToString().ToLower()
- },
- CanHandleArguments = name switch
- {
- ApplicationName.IPScanner => true,
- ApplicationName.PortScanner => true,
- ApplicationName.PingMonitor => true,
- ApplicationName.Traceroute => true,
- ApplicationName.DNSLookup => true,
- ApplicationName.RemoteDesktop => true,
- ApplicationName.PowerShell => true,
- ApplicationName.PuTTY => true,
- ApplicationName.TigerVNC => true,
- ApplicationName.WebConsole => true,
- ApplicationName.SNMP => true,
- ApplicationName.RDAP => true,
- ApplicationName.Whois => true,
- _ => false
- },
- ExampleArguments = name switch
+ // The name is compared like it is parsed (without surrounding whitespace and quotes).
+ var profiles = GetProfiles(result.Command)
+ .Where(x => string.Equals(x.Name.Trim().Trim('"').Trim(), result.ProfileName,
+ StringComparison.OrdinalIgnoreCase))
+ .ToList();
+
+ switch (profiles.Count)
{
- ApplicationName.IPScanner => "<10.0.0.0/24>",
- ApplicationName.PortScanner => "<10.0.0.10;10.0.0.20> <1-1024>",
- ApplicationName.PingMonitor => DefaultArguments,
- ApplicationName.Traceroute => DefaultArguments,
- ApplicationName.DNSLookup => " ",
- ApplicationName.RemoteDesktop => DefaultArguments,
- ApplicationName.PowerShell => DefaultArguments,
- ApplicationName.PuTTY => DefaultArguments,
- ApplicationName.TigerVNC => DefaultArguments,
- ApplicationName.WebConsole => "",
- ApplicationName.SNMP => DefaultArguments,
- ApplicationName.RDAP => "",
- ApplicationName.Whois => "",
- _ => string.Empty
+ case 0:
+ return string.Format(Strings.ProfileXNotFound, result.ProfileName);
+ case > 1:
+ return string.Format(Strings.SeveralProfilesAreNamedXSelectOne, result.ProfileName);
}
- }).ToList();
+
+ profile = profiles[0];
+ }
+
+ request = new ApplicationRequest(result.Command.Application) { Profile = profile };
+
+ return null;
+ }
+
+ // Only open the application
+ if (result.Target == null)
+ {
+ request = new ApplicationRequest(result.Command.Application);
+
+ return null;
+ }
+
+ error = ValidateTarget(result);
+
+ if (error != null)
+ return error;
+
+ request = CreateRequest(result);
+
+ return null;
+ }
+
+ ///
+ /// Gets the message for a parse error.
+ ///
+ private static string GetParseErrorMessage(RunCommandParseResult result)
+ {
+ return result.Error switch
+ {
+ RunCommandParseError.None => null,
+ RunCommandParseError.Empty => Strings.EnterACommand,
+ RunCommandParseError.UnknownCommand => string.Format(Strings.UnknownCommandX, result.ErrorValue),
+ RunCommandParseError.NoArguments => string.Format(Strings.CommandXDoesNotAcceptArguments, result.ErrorValue),
+ RunCommandParseError.NoProfiles => string.Format(Strings.CommandXDoesNotSupportProfiles, result.ErrorValue),
+ RunCommandParseError.MissingProfileName => Strings.EnterTheNameOfAProfileAfterAt,
+ RunCommandParseError.UnknownArgument => string.Format(Strings.UnknownArgumentX, result.ErrorValue),
+ _ => throw new ArgumentOutOfRangeException(nameof(result), result.Error, null)
+ };
+ }
+
+ ///
+ /// Validates the target and the second argument with the validators of the application's input fields.
+ ///
+ private static string ValidateTarget(RunCommandParseResult result)
+ {
+ ValidationRule targetValidator = result.Command.Application switch
+ {
+ ApplicationName.IPScanner or ApplicationName.PortScanner or ApplicationName.PingMonitor =>
+ new MultipleHostsRangeValidator(),
+ ApplicationName.Traceroute or ApplicationName.PowerShell or ApplicationName.PuTTY
+ or ApplicationName.TigerVNC => new IPAddressOrHostnameValidator(),
+ ApplicationName.RemoteDesktop => new RemoteDesktopHostnameAndPortValidator(),
+ ApplicationName.WebConsole => new HttpAndHttpsUriValidator(),
+ ApplicationName.Whois => new DomainValidator(),
+ ApplicationName.IPGeolocation => new IPAddressOrHostnameValidator(),
+ ApplicationName.WakeOnLAN => new MACAddressValidator(),
+ _ => null
+ };
+
+ var error = Validate(targetValidator, result.Target);
+
+ if (error != null)
+ return error;
+
+ if (result.Argument != null)
+ {
+ ValidationRule argumentValidator = result.Command.Argument switch
+ {
+ RunCommandArgument.Ports => new PortRangeValidator(),
+ RunCommandArgument.Broadcast => new IPv4AddressValidator(),
+ _ => null
+ };
+
+ error = result.Command.Argument == RunCommandArgument.DNSServer
+ ? ValidateDNSServer(result.Argument)
+ : Validate(argumentValidator, result.Argument);
+
+ if (error != null)
+ return error;
+ }
+
+ // RDAP: the query must match the type (e.g. "1.10" as ASN)
+ if (result.Command.Application == ApplicationName.RDAP)
+ {
+ var parseError = RDAPQueryParser.TryParse(GetRDAPQueryType(result.Type), result.Target, out _);
+
+ if (parseError != RDAPQueryParseError.None)
+ return RDAPQueryValidator.GetErrorMessage(parseError);
+ }
+
+ return null;
+ }
+
+ ///
+ /// Validates the DNS server argument: the name of a configured DNS server, or custom DNS server(s) in the format
+ /// of the DNS lookup (IP address or host with a domain, optional port). A single word that is neither (e.g. a
+ /// mistyped type like "aaa") is an unknown argument instead of a DNS server.
+ ///
+ private static string ValidateDNSServer(string dnsServer)
+ {
+ if (GetDNSServers().Any(x => string.Equals(x, dnsServer, StringComparison.OrdinalIgnoreCase)))
+ return null;
+
+ foreach (var server in dnsServer.Split(';',
+ StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
+ {
+ if (!ServerConnectionInfo.TryParse(server, out var serverInfo, 53, TransportProtocol.Udp) ||
+ !IPAddress.TryParse(serverInfo.Server, out _) && !serverInfo.Server.Contains('.'))
+ return string.Format(Strings.UnknownArgumentX, server);
+ }
+
+ return null;
+ }
+
+ private static string Validate(ValidationRule validator, string value)
+ {
+ if (validator == null)
+ return null;
+
+ var validationResult = validator.Validate(value, CultureInfo.CurrentCulture);
+
+ return validationResult.IsValid ? null : validationResult.ErrorContent as string;
+ }
+
+ ///
+ /// Creates the request with the options of the application.
+ ///
+ private static ApplicationRequest CreateRequest(RunCommandParseResult result)
+ {
+ return result.Command.Application switch
+ {
+ ApplicationName.DNSLookup => new DNSLookupRequest
+ {
+ Target = result.Target,
+ // A configured DNS server is matched by its name (case-insensitive), otherwise it's a custom server.
+ DNSServer = GetDNSServers().FirstOrDefault(x =>
+ string.Equals(x, result.Argument, StringComparison.OrdinalIgnoreCase)) ?? result.Argument,
+ // Without a type, the DNS lookup uses PTR for IP addresses and its selected type for hosts.
+ QueryType = result.Type != null ? Enum.Parse(result.Type, true) : null
+ },
+ ApplicationName.PortScanner => new PortScannerRequest { Target = result.Target, Ports = result.Argument },
+ ApplicationName.WakeOnLAN => new WakeOnLANRequest { Target = result.Target, Broadcast = result.Argument },
+ ApplicationName.RDAP => new RDAPRequest { Target = result.Target, QueryType = GetRDAPQueryType(result.Type) },
+ _ => new ApplicationRequest(result.Command.Application) { Target = result.Target }
+ };
+ }
+
+ private static RDAPQueryType GetRDAPQueryType(string type)
+ {
+ return type != null ? RDAPTypes[type] : RDAPQueryType.Auto;
+ }
+
+ ///
+ /// Creates the commands of all applications and the settings. Commands and aliases are lowercase English keywords
+ /// (not translated); the applications are shown with their translated name.
+ ///
+ private static List CreateCommands()
+ {
+ // Syntax of the arguments, e.g. " [Ports]"
+ static string Required(string name) => $"<{name}>";
+ static string Optional(string name) => $"[{name}]";
+
+ var commands = ApplicationManager.GetNames().Where(x => x != ApplicationName.None).Select(name => name switch
+ {
+ ApplicationName.IPScanner => Create(name, ["scan"], Required(Strings.IPRange), true),
+ ApplicationName.PortScanner => Create(name, ["portscan"],
+ $"{Required(Strings.Hosts)} {Optional(Strings.Ports)}", true, RunCommandArgument.Ports),
+ ApplicationName.PingMonitor => Create(name, ["ping"], Required(Strings.Hosts), true),
+ ApplicationName.Traceroute => Create(name, ["trace", "tracert"], Required(Strings.Host), true),
+ ApplicationName.DNSLookup => Create(name, ["dns", "nslookup"],
+ $"{Required(Strings.Hosts)} {Optional(Strings.DNSServer)} {Optional(Strings.Type)}", true,
+ RunCommandArgument.DNSServer,
+ [.. DNSLookup.QueryTypes.Select(x => x.ToString().ToLowerInvariant()).Order()]),
+ ApplicationName.RemoteDesktop => Create(name, ["rdp", "mstsc"],
+ $"{Required(Strings.Host)}[:{Required(Strings.Port)}]", true),
+ ApplicationName.PowerShell => Create(name, ["ps", "pwsh"], Required(Strings.Host), true),
+ ApplicationName.PuTTY => Create(name, ["ssh"], Required(Strings.Host), true),
+ ApplicationName.TigerVNC => Create(name, ["vnc"], Required(Strings.Host), true),
+ ApplicationName.WebConsole => Create(name, ["web"], Required(Strings.URL), true),
+ ApplicationName.SNMP => Create(name, [], Required(Strings.Host), true),
+ ApplicationName.RDAP => Create(name, [], $"{Required(Strings.Query)} {Optional(Strings.Type)}", true,
+ types: [.. RDAPTypes.Keys]),
+ ApplicationName.Whois => Create(name, [], Required(Strings.Domain), true),
+ ApplicationName.IPGeolocation => Create(name, ["geo"], Required(Strings.Host), true),
+ ApplicationName.WakeOnLAN => Create(name, ["wol"],
+ $"{Required(Strings.MACAddress)} {Optional(Strings.Broadcast)}", true, RunCommandArgument.Broadcast),
+ _ => Create(name, [], string.Empty, false)
+ }).ToList();
+
+ commands.Add(new RunCommandDefinition
+ {
+ Type = RunCommandType.Setting,
+ Application = ApplicationName.None,
+ Command = "settings"
+ });
+
+ return commands;
+ }
+
+ private static RunCommandDefinition Create(ApplicationName application, string[] aliases, string syntax,
+ bool supportsTarget, RunCommandArgument argument = RunCommandArgument.None, string[] types = null)
+ {
+ return new RunCommandDefinition
+ {
+ Type = RunCommandType.Application,
+ Application = application,
+ Command = application.ToString().ToLowerInvariant(),
+ Aliases = aliases,
+ SupportsTarget = supportsTarget,
+ // Every application that accepts a target also supports profiles.
+ SupportsProfile = supportsTarget,
+ Argument = argument,
+ Types = types ?? [],
+ Syntax = supportsTarget ? $"{syntax} | @<{Strings.Profile}>" : string.Empty
+ };
}
-}
\ No newline at end of file
+}
diff --git a/Source/NETworkManager/RunCommandSuggestion.cs b/Source/NETworkManager/RunCommandSuggestion.cs
new file mode 100644
index 0000000000..cd2d32bd85
--- /dev/null
+++ b/Source/NETworkManager/RunCommandSuggestion.cs
@@ -0,0 +1,75 @@
+using NETworkManager.Models;
+using NETworkManager.Profiles;
+
+namespace NETworkManager;
+
+///
+/// Kind of a suggestion in the run command dialog.
+///
+public enum RunCommandSuggestionKind
+{
+ ///
+ /// Run the input as typed.
+ ///
+ Run,
+
+ ///
+ /// Command of an application (or the settings).
+ ///
+ Command,
+
+ ///
+ /// Profile of the application.
+ ///
+ Profile,
+
+ ///
+ /// Recently run command.
+ ///
+ History,
+
+ ///
+ /// Type keyword of the command (e.g. DNS record type).
+ ///
+ Type,
+
+ ///
+ /// DNS server configured in the DNS lookup.
+ ///
+ DNSServer
+}
+
+///
+/// Suggestion in the run command dialog.
+///
+public sealed class RunCommandSuggestion
+{
+ public RunCommandSuggestionKind Kind { get; init; }
+
+ ///
+ /// Application of the suggestion (for the icon), for the settings.
+ ///
+ public ApplicationName Application { get; init; }
+
+ public string Title { get; init; }
+
+ ///
+ /// Second line (e.g. group and host of a profile).
+ ///
+ public string Description { get; init; }
+
+ ///
+ /// Text on the right (e.g. "@name" of a profile).
+ ///
+ public string Info { get; init; }
+
+ ///
+ /// Input after completing the suggestion (Tab) and that is run (Enter).
+ ///
+ public string CompletionText { get; init; }
+
+ ///
+ /// Profile to run ().
+ ///
+ public ProfileInfo Profile { get; init; }
+}
diff --git a/Source/NETworkManager/RunCommandType.cs b/Source/NETworkManager/RunCommandType.cs
deleted file mode 100644
index ba94a8abc0..0000000000
--- a/Source/NETworkManager/RunCommandType.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace NETworkManager;
-
-public enum RunCommandType
-{
- Application,
-
- Setting
-}
\ No newline at end of file
diff --git a/Source/NETworkManager/ViewModels/BitCalculatorViewModel.cs b/Source/NETworkManager/ViewModels/BitCalculatorViewModel.cs
index 01a24481e0..9d9141aa88 100644
--- a/Source/NETworkManager/ViewModels/BitCalculatorViewModel.cs
+++ b/Source/NETworkManager/ViewModels/BitCalculatorViewModel.cs
@@ -264,27 +264,10 @@ private async void Calculate()
IsResultVisible = true;
- AddInputToHistory(Input);
-
- IsRunning = false;
- }
-
- ///
- /// Adds the input to the history.
- ///
- /// The input string.
- private void AddInputToHistory(string input)
- {
- // Create the new list
- var list = ListHelper.Modify(SettingsManager.Current.BitCalculator_InputHistory.ToList(), input,
+ ListHelper.Modify(SettingsManager.Current.BitCalculator_InputHistory, Input,
SettingsManager.Current.General_HistoryListEntries);
- // Clear the old items
- SettingsManager.Current.BitCalculator_InputHistory.Clear();
- OnPropertyChanged(nameof(Input)); // Raise property changed again, after the collection has been cleared
-
- // Fill with the new items
- list.ForEach(x => SettingsManager.Current.BitCalculator_InputHistory.Add(x));
+ IsRunning = false;
}
///
diff --git a/Source/NETworkManager/ViewModels/DNSLookupHostViewModel.cs b/Source/NETworkManager/ViewModels/DNSLookupHostViewModel.cs
index 14e9865f44..1fecad4366 100644
--- a/Source/NETworkManager/ViewModels/DNSLookupHostViewModel.cs
+++ b/Source/NETworkManager/ViewModels/DNSLookupHostViewModel.cs
@@ -1,3 +1,4 @@
+using DnsClient;
using Dragablz;
using NETworkManager.Controls;
using NETworkManager.Localization.Resources;
@@ -8,6 +9,9 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
+using System.Linq;
+using System.Net;
+using System.Net.Sockets;
using System.Windows.Input;
namespace NETworkManager.ViewModels;
@@ -89,10 +93,6 @@ public DNSLookupHostViewModel()
protected override ApplicationName ApplicationName => ApplicationName.DNSLookup;
- protected override bool IsProfileEnabled(ProfileInfo profile) => profile.DNSLookup_Enabled;
-
- protected override string GetSearchableField(ProfileInfo profile) => profile.DNSLookup_Host;
-
#endregion
#region ICommand & Actions
@@ -128,7 +128,7 @@ private bool LookupProfile_CanExecute(object obj)
///
private void LookupProfileAction()
{
- AddTab(SelectedProfile.DNSLookup_Host);
+ AddTab(SelectedProfile);
}
///
@@ -152,15 +152,43 @@ private static void CloseItemAction(ItemActionCallbackArgs args)
/// Adds a new tab for the specified host.
///
/// The host to lookup.
- public void AddTab(string host = null)
+ /// DNS server(s) to use. If null, the selected DNS server is used.
+ ///
+ /// Query type. If null, PTR is used for IP addresses (e.g. from the IP Scanner) and the selected query type for
+ /// hosts.
+ ///
+ public void AddTab(string host = null, string dnsServer = null, QueryType? queryType = null)
{
+ if (queryType == null && IsIPAddresses(host))
+ queryType = QueryType.PTR;
+
var tabId = Guid.NewGuid();
- TabItems.Add(new DragablzTabItem(host ?? Strings.NewTab, new DNSLookupView(tabId, host),
+ TabItems.Add(new DragablzTabItem(host ?? Strings.NewTab, new DNSLookupView(tabId, host, dnsServer, queryType),
tabId));
SelectedTabIndex = TabItems.Count - 1;
}
+ ///
+ /// Checks if the host(s) are only IP addresses (shortened IPv4 forms like "10" are host names).
+ ///
+ private static bool IsIPAddresses(string hosts)
+ {
+ return !string.IsNullOrWhiteSpace(hosts) && hosts.Split(';').Select(x => x.Trim()).All(x =>
+ RegexHelper.IPv4AddressRegex().IsMatch(x) ||
+ x.Contains(':') && IPAddress.TryParse(x, out var ipAddress) &&
+ ipAddress.AddressFamily == AddressFamily.InterNetworkV6);
+ }
+
+ ///
+ /// Adds a new tab for the host of the specified profile.
+ ///
+ /// The profile to lookup.
+ public void AddTab(ProfileInfo profile)
+ {
+ AddTab(profile.DNSLookup_Host);
+ }
+
#endregion
}
diff --git a/Source/NETworkManager/ViewModels/DNSLookupViewModel.cs b/Source/NETworkManager/ViewModels/DNSLookupViewModel.cs
index 0f38a429a9..ac2b0d9122 100644
--- a/Source/NETworkManager/ViewModels/DNSLookupViewModel.cs
+++ b/Source/NETworkManager/ViewModels/DNSLookupViewModel.cs
@@ -248,7 +248,9 @@ private set
///
/// The ID of the tab.
/// The host to lookup.
- public DNSLookupViewModel(Guid tabId, string host)
+ /// DNS server(s) to use (not saved as default). If null, the last selected one is used.
+ /// Query type (not saved as default). If null, the last selected one is used.
+ public DNSLookupViewModel(Guid tabId, string host, string dnsServer = null, QueryType? queryType = null)
{
_isLoading = true;
@@ -276,6 +278,13 @@ public DNSLookupViewModel(Guid tabId, string host)
LoadSettings();
+ // Options of a request (e.g. run command), which are not saved as default.
+ if (!string.IsNullOrEmpty(dnsServer))
+ DNSServer = dnsServer;
+
+ if (queryType != null && QueryTypes.Contains(queryType.Value))
+ QueryType = queryType.Value;
+
_isLoading = false;
}
@@ -376,7 +385,8 @@ private async Task QueryAsync()
DragablzTabItem.SetTabHeader(_tabId, Host);
- AddHostToHistory(Host);
+ ListHelper.Modify(SettingsManager.Current.DNSLookup_HostHistory, Host,
+ SettingsManager.Current.General_HistoryListEntries);
DNSLookupSettings dnsSettings = new()
{
@@ -508,24 +518,6 @@ private void AppendStatusMessage(string message)
}
// Modify history list
- ///
- /// Adds the host to the history.
- ///
- /// The host to add.
- private void AddHostToHistory(string host)
- {
- // Create the new list
- var list = ListHelper.Modify([.. SettingsManager.Current.DNSLookup_HostHistory], host,
- SettingsManager.Current.General_HistoryListEntries);
-
- // Clear the old items
- SettingsManager.Current.DNSLookup_HostHistory.Clear();
- OnPropertyChanged(nameof(Host)); // Raise property changed again, after the collection has been cleared
-
- // Fill with the new items
- list.ForEach(x => SettingsManager.Current.DNSLookup_HostHistory.Add(x));
- }
-
///
/// Exports the results.
diff --git a/Source/NETworkManager/ViewModels/IPGeolocationHostViewModel.cs b/Source/NETworkManager/ViewModels/IPGeolocationHostViewModel.cs
index dc6cac0a84..3b1e44685a 100644
--- a/Source/NETworkManager/ViewModels/IPGeolocationHostViewModel.cs
+++ b/Source/NETworkManager/ViewModels/IPGeolocationHostViewModel.cs
@@ -88,10 +88,6 @@ public IPGeolocationHostViewModel()
protected override ApplicationName ApplicationName => ApplicationName.IPGeolocation;
- protected override bool IsProfileEnabled(ProfileInfo profile) => profile.IPGeolocation_Enabled;
-
- protected override string GetSearchableField(ProfileInfo profile) => profile.IPGeolocation_Host;
-
#endregion
#region ICommand & Actions
@@ -127,7 +123,7 @@ private bool QueryProfile_CanExecute(object obj)
///
private void QueryProfileAction()
{
- AddTab(SelectedProfile.IPGeolocation_Host);
+ AddTab(SelectedProfile);
}
///
@@ -151,7 +147,7 @@ private static void CloseItemAction(ItemActionCallbackArgs args)
/// Adds a new tab for the specified domain.
///
/// The domain to query.
- private void AddTab(string domain = null)
+ public void AddTab(string domain = null)
{
var tabId = Guid.NewGuid();
@@ -161,5 +157,14 @@ private void AddTab(string domain = null)
SelectedTabIndex = TabItems.Count - 1;
}
+ ///
+ /// Adds a new tab for the host of the specified profile.
+ ///
+ /// The profile to query.
+ public void AddTab(ProfileInfo profile)
+ {
+ AddTab(profile.IPGeolocation_Host);
+ }
+
#endregion
}
diff --git a/Source/NETworkManager/ViewModels/IPGeolocationViewModel.cs b/Source/NETworkManager/ViewModels/IPGeolocationViewModel.cs
index 40aa8d54e3..ea79459e4e 100644
--- a/Source/NETworkManager/ViewModels/IPGeolocationViewModel.cs
+++ b/Source/NETworkManager/ViewModels/IPGeolocationViewModel.cs
@@ -262,7 +262,8 @@ private async Task Query()
Result = result.Info;
IsResultVisible = true;
- AddHostToHistory(Host);
+ ListHelper.Modify(SettingsManager.Current.IPGeolocation_HostHistory, Host,
+ SettingsManager.Current.General_HistoryListEntries);
}
catch (Exception ex)
{
@@ -335,23 +336,5 @@ public void OnClose()
ConfigurationManager.Current.IPGeolocationTabCount--;
}
- ///
- /// Adds the host to the history.
- ///
- /// The host to add.
- private void AddHostToHistory(string host)
- {
- // Create the new list
- var list = ListHelper.Modify(SettingsManager.Current.IPGeolocation_HostHistory.ToList(), host,
- SettingsManager.Current.General_HistoryListEntries);
-
- // Clear the old items
- SettingsManager.Current.IPGeolocation_HostHistory.Clear();
- OnPropertyChanged(nameof(Host)); // Raise property changed again, after the collection has been cleared
-
- // Fill with the new items
- list.ForEach(x => SettingsManager.Current.IPGeolocation_HostHistory.Add(x));
- }
-
#endregion
}
\ No newline at end of file
diff --git a/Source/NETworkManager/ViewModels/IPScannerHostViewModel.cs b/Source/NETworkManager/ViewModels/IPScannerHostViewModel.cs
index 9ca9e59493..4808a0e2c9 100644
--- a/Source/NETworkManager/ViewModels/IPScannerHostViewModel.cs
+++ b/Source/NETworkManager/ViewModels/IPScannerHostViewModel.cs
@@ -88,10 +88,6 @@ public IPScannerHostViewModel()
protected override ApplicationName ApplicationName => ApplicationName.IPScanner;
- protected override bool IsProfileEnabled(ProfileInfo profile) => profile.IPScanner_Enabled;
-
- protected override string GetSearchableField(ProfileInfo profile) => profile.IPScanner_HostOrIPRange;
-
#endregion
#region ICommand & Actions
@@ -165,7 +161,7 @@ public void AddTab(string hostOrIPRange = null)
/// Adds a new tab for the specified profile.
///
/// The profile to scan.
- private void AddTab(ProfileInfo profile)
+ public void AddTab(ProfileInfo profile)
{
AddTab(profile.IPScanner_HostOrIPRange);
}
diff --git a/Source/NETworkManager/ViewModels/IPScannerViewModel.cs b/Source/NETworkManager/ViewModels/IPScannerViewModel.cs
index 4905fdb68d..d4fe8eeafc 100644
--- a/Source/NETworkManager/ViewModels/IPScannerViewModel.cs
+++ b/Source/NETworkManager/ViewModels/IPScannerViewModel.cs
@@ -5,7 +5,6 @@
using NETworkManager.Localization;
using NETworkManager.Localization.Resources;
using NETworkManager.Models;
-using NETworkManager.Models.EventSystem;
using NETworkManager.Models.Export;
using NETworkManager.Models.Network;
using NETworkManager.Profiles;
@@ -385,7 +384,7 @@ private void RedirectDataToApplicationAction(object name)
? SelectedResult.Hostname
: SelectedResult.PingInfo.IPAddress.ToString();
- EventSystem.RedirectToApplication(applicationName, host);
+ NavigationManager.Open(new ApplicationRequest(applicationName) { Target = host });
}
///
@@ -395,7 +394,7 @@ private void RedirectDataToApplicationAction(object name)
private void PerformDNSLookupIPAddressAction()
{
- EventSystem.RedirectToApplication(ApplicationName.DNSLookup, SelectedResult.PingInfo.IPAddress.ToString());
+ NavigationManager.Open(new DNSLookupRequest { Target = SelectedResult.PingInfo.IPAddress.ToString() });
}
///
@@ -405,7 +404,7 @@ private void PerformDNSLookupIPAddressAction()
private void PerformDNSLookupHostnameAction()
{
- EventSystem.RedirectToApplication(ApplicationName.DNSLookup, SelectedResult.Hostname);
+ NavigationManager.Open(new DNSLookupRequest { Target = SelectedResult.Hostname });
}
///
@@ -415,7 +414,7 @@ private void PerformDNSLookupHostnameAction()
private void PerformRDAPQueryIPAddressAction()
{
- EventSystem.RedirectToApplication(ApplicationName.RDAP, SelectedResult.PingInfo.IPAddress.ToString());
+ NavigationManager.Open(new RDAPRequest { Target = SelectedResult.PingInfo.IPAddress.ToString() });
}
///
@@ -425,7 +424,7 @@ private void PerformRDAPQueryIPAddressAction()
private void PerformRDAPQueryHostnameAction()
{
- EventSystem.RedirectToApplication(ApplicationName.RDAP, SelectedResult.Hostname);
+ NavigationManager.Open(new RDAPRequest { Target = SelectedResult.Hostname });
}
///
@@ -553,7 +552,8 @@ private async Task Start()
PreparingScan = false;
// Add host(s) to the history
- AddHostToHistory(Host);
+ ListHelper.Modify(SettingsManager.Current.IPScanner_HostHistory, Host,
+ SettingsManager.Current.General_HistoryListEntries);
var ipScanner = new IPScanner(new IPScannerOptions(
SettingsManager.Current.IPScanner_MaxHostThreads,
@@ -700,24 +700,6 @@ private async Task CustomCommand(object guid)
}
}
- ///
- /// Adds the scanned host/range to the history.
- ///
- /// The host or IP range to add.
- private void AddHostToHistory(string ipRange)
- {
- // Create the new list
- var list = ListHelper.Modify([.. SettingsManager.Current.IPScanner_HostHistory], ipRange,
- SettingsManager.Current.General_HistoryListEntries);
-
- // Clear the old items
- SettingsManager.Current.IPScanner_HostHistory.Clear();
- OnPropertyChanged(nameof(Host)); // Raise property changed again, after the collection has been cleared
-
- // Fill with the new items
- list.ForEach(SettingsManager.Current.IPScanner_HostHistory.Add);
- }
-
///
/// Exports the scan results.
///
diff --git a/Source/NETworkManager/ViewModels/LookupOUILookupViewModel.cs b/Source/NETworkManager/ViewModels/LookupOUILookupViewModel.cs
index fdd320882b..9ae439de75 100644
--- a/Source/NETworkManager/ViewModels/LookupOUILookupViewModel.cs
+++ b/Source/NETworkManager/ViewModels/LookupOUILookupViewModel.cs
@@ -43,28 +43,6 @@ public LookupOUILookupViewModel()
#endregion
- #region Methods
-
- ///
- /// Adds the search term to the history.
- ///
- /// The MAC address or vendor.
- private void AddSearchToHistory(string macAddressOrVendor)
- {
- // Create the new list
- var list = ListHelper.Modify(SettingsManager.Current.Lookup_OUI_SearchHistory.ToList(),
- macAddressOrVendor, SettingsManager.Current.General_HistoryListEntries);
-
- // Clear the old items
- SettingsManager.Current.Lookup_OUI_SearchHistory.Clear();
- OnPropertyChanged(
- nameof(Search)); // Raise property changed again, after the collection has been cleared
-
- // Fill with the new items
- list.ForEach(x => SettingsManager.Current.Lookup_OUI_SearchHistory.Add(x));
- }
-
- #endregion
#region Variables
@@ -260,7 +238,8 @@ private async void OUILookupAction()
NothingFound = Results.Count == 0;
// Add the MAC-Address or Vendor to the history
- AddSearchToHistory(Search);
+ ListHelper.Modify(SettingsManager.Current.Lookup_OUI_SearchHistory, Search,
+ SettingsManager.Current.General_HistoryListEntries);
IsRunning = false;
}
diff --git a/Source/NETworkManager/ViewModels/LookupPortViewModel.cs b/Source/NETworkManager/ViewModels/LookupPortViewModel.cs
index 62e6aea1e6..955e6f8ee8 100644
--- a/Source/NETworkManager/ViewModels/LookupPortViewModel.cs
+++ b/Source/NETworkManager/ViewModels/LookupPortViewModel.cs
@@ -40,27 +40,6 @@ public LookupPortLookupViewModel()
#endregion
- #region Methods
-
- ///
- /// Adds the search term to the history.
- ///
- /// The port or service.
- private void AddSearchToHistory(string portOrService)
- {
- // Create the new list
- var list = ListHelper.Modify(SettingsManager.Current.Lookup_Port_SearchHistory.ToList(), portOrService,
- SettingsManager.Current.General_HistoryListEntries);
-
- // Clear the old items
- SettingsManager.Current.Lookup_Port_SearchHistory.Clear();
- OnPropertyChanged(nameof(Search)); // Raise property changed again, after the collection has been cleared
-
- // Fill with the new items
- list.ForEach(x => SettingsManager.Current.Lookup_Port_SearchHistory.Add(x));
- }
-
- #endregion
#region Variables
@@ -320,7 +299,8 @@ await PortLookup.LookupByPortAndProtocolAsync(
NothingFound = Results.Count == 0;
// Add the MAC-Address or Vendor to the history
- AddSearchToHistory(Search);
+ ListHelper.Modify(SettingsManager.Current.Lookup_Port_SearchHistory, Search,
+ SettingsManager.Current.General_HistoryListEntries);
IsRunning = false;
}
diff --git a/Source/NETworkManager/ViewModels/NetworkInterfaceViewModel.cs b/Source/NETworkManager/ViewModels/NetworkInterfaceViewModel.cs
index dc48aa71b0..70f1e90188 100644
--- a/Source/NETworkManager/ViewModels/NetworkInterfaceViewModel.cs
+++ b/Source/NETworkManager/ViewModels/NetworkInterfaceViewModel.cs
@@ -9,7 +9,6 @@
using MahApps.Metro.SimpleChildWindow;
using NETworkManager.Localization.Resources;
using NETworkManager.Models;
-using NETworkManager.Models.EventSystem;
using NETworkManager.Models.Export;
using NETworkManager.Models.Network;
using NETworkManager.Profiles;
@@ -733,11 +732,6 @@ private async Task LoadNetworkInterfaces()
protected override ApplicationName ApplicationName => ApplicationName.NetworkInterface;
- protected override bool IsProfileEnabled(ProfileInfo profile) => profile.NetworkInterface_Enabled;
-
- // Unlike other tools, profile search here only matches the profile name (no secondary field).
- protected override string GetSearchableField(ProfileInfo profile) => string.Empty;
-
#endregion
#region ICommands & Actions
@@ -899,8 +893,10 @@ private void IPScannerAction()
if (ipTuple == null)
return;
- EventSystem.RedirectToApplication(ApplicationName.IPScanner,
- $"{ipTuple.Item1}/{Subnetmask.ConvertSubnetmaskToCidr(ipTuple.Item2)}");
+ NavigationManager.Open(new ApplicationRequest(ApplicationName.IPScanner)
+ {
+ Target = $"{ipTuple.Item1}/{Subnetmask.ConvertSubnetmaskToCidr(ipTuple.Item2)}"
+ });
}
///
diff --git a/Source/NETworkManager/ViewModels/PingMonitorHostViewModel.cs b/Source/NETworkManager/ViewModels/PingMonitorHostViewModel.cs
index a25bf063a8..28a7e46956 100644
--- a/Source/NETworkManager/ViewModels/PingMonitorHostViewModel.cs
+++ b/Source/NETworkManager/ViewModels/PingMonitorHostViewModel.cs
@@ -185,10 +185,6 @@ public PingMonitorHostViewModel()
protected override ApplicationName ApplicationName => ApplicationName.PingMonitor;
- protected override bool IsProfileEnabled(ProfileInfo profile) => profile.PingMonitor_Enabled;
-
- protected override string GetSearchableField(ProfileInfo profile) => profile.PingMonitor_Host;
-
#endregion
#region ICommands & Actions
@@ -225,8 +221,7 @@ private bool PingProfile_CanExecute(object obj)
private void PingProfileAction()
{
- if (SetHost(SelectedProfile.PingMonitor_Host, SelectedProfile.Group))
- _ = Start();
+ AddHost(SelectedProfile);
}
///
@@ -265,6 +260,26 @@ private void PauseGroupAction(object group)
#region Methods
+ ///
+ /// Adds the host(s) and starts the ping monitor.
+ ///
+ /// Host(s) to ping.
+ public void AddHost(string host)
+ {
+ if (SetHost(host))
+ _ = Start();
+ }
+
+ ///
+ /// Adds the host(s) of the profile to its group and starts the ping monitor.
+ ///
+ /// Profile to ping.
+ public void AddHost(ProfileInfo profile)
+ {
+ if (SetHost(profile.PingMonitor_Host, profile.Group))
+ _ = Start();
+ }
+
///
/// Set the host to ping.
///
@@ -325,7 +340,8 @@ public async Task Start()
}
// Add host(s) to history
- AddHostToHistory(Host);
+ ListHelper.Modify(SettingsManager.Current.PingMonitor_HostHistory, Host,
+ SettingsManager.Current.General_HistoryListEntries);
// Add host(s) to list and start the ping
foreach (var hostView in hosts.hosts.Select(currentHost =>
@@ -390,20 +406,6 @@ private void RemoveHostByGuid(Guid hostId)
Hosts.RemoveAt(i);
}
- private void AddHostToHistory(string host)
- {
- // Create the new list
- var list = ListHelper.Modify([.. SettingsManager.Current.PingMonitor_HostHistory], host,
- SettingsManager.Current.General_HistoryListEntries);
-
- // Clear the old items
- SettingsManager.Current.PingMonitor_HostHistory.Clear();
- OnPropertyChanged(nameof(Host)); // Raise property changed again, after the collection has been cleared
-
- // Fill with the new items
- list.ForEach(SettingsManager.Current.PingMonitor_HostHistory.Add);
- }
-
#endregion
#region Event
diff --git a/Source/NETworkManager/ViewModels/PortScannerHostViewModel.cs b/Source/NETworkManager/ViewModels/PortScannerHostViewModel.cs
index d2b982af47..a83df00221 100644
--- a/Source/NETworkManager/ViewModels/PortScannerHostViewModel.cs
+++ b/Source/NETworkManager/ViewModels/PortScannerHostViewModel.cs
@@ -88,10 +88,6 @@ public PortScannerHostViewModel()
protected override ApplicationName ApplicationName => ApplicationName.PortScanner;
- protected override bool IsProfileEnabled(ProfileInfo profile) => profile.PortScanner_Enabled;
-
- protected override string GetSearchableField(ProfileInfo profile) => profile.PortScanner_Host;
-
#endregion
#region ICommand & Actions
@@ -150,7 +146,7 @@ public void AddTab(string host = null, string ports = null)
SelectedTabIndex = TabItems.Count - 1;
}
- private void AddTab(ProfileInfo profile)
+ public void AddTab(ProfileInfo profile)
{
AddTab(profile.PortScanner_Host, profile.PortScanner_Ports);
}
diff --git a/Source/NETworkManager/ViewModels/PortScannerViewModel.cs b/Source/NETworkManager/ViewModels/PortScannerViewModel.cs
index 926037f288..d455551963 100644
--- a/Source/NETworkManager/ViewModels/PortScannerViewModel.cs
+++ b/Source/NETworkManager/ViewModels/PortScannerViewModel.cs
@@ -487,8 +487,10 @@ private async Task Start()
PreparingScan = false;
// Add host(s) to the history
- AddHostToHistory(Host);
- AddPortToHistory(Ports);
+ ListHelper.Modify(SettingsManager.Current.PortScanner_HostHistory, Host,
+ SettingsManager.Current.General_HistoryListEntries);
+ ListHelper.Modify(SettingsManager.Current.PortScanner_PortHistory, Ports,
+ SettingsManager.Current.General_HistoryListEntries);
var portScanner = new PortScanner(new PortScannerOptions(
SettingsManager.Current.PortScanner_MaxHostThreads,
@@ -571,34 +573,6 @@ await DialogHelper.ShowMessageAsync(window, Strings.Error,
return window.ShowChildWindowAsync(childWindow);
}
- private void AddHostToHistory(string host)
- {
- // Create the new list
- var list = ListHelper.Modify(SettingsManager.Current.PortScanner_HostHistory.ToList(), host,
- SettingsManager.Current.General_HistoryListEntries);
-
- // Clear the old items
- SettingsManager.Current.PortScanner_HostHistory.Clear();
- OnPropertyChanged(nameof(Host)); // Raise property changed again, after the collection has been cleared
-
- // Fill with the new items
- list.ForEach(x => SettingsManager.Current.PortScanner_HostHistory.Add(x));
- }
-
- private void AddPortToHistory(string port)
- {
- // Create the new list
- var list = ListHelper.Modify(SettingsManager.Current.PortScanner_PortHistory.ToList(), port,
- SettingsManager.Current.General_HistoryListEntries);
-
- // Clear the old items
- SettingsManager.Current.PortScanner_PortHistory.Clear();
- OnPropertyChanged(nameof(Ports)); // Raise property changed again, after the collection has been cleared
-
- // Fill with the new items
- list.ForEach(x => SettingsManager.Current.PortScanner_PortHistory.Add(x));
- }
-
#endregion
#region Events
diff --git a/Source/NETworkManager/ViewModels/PowerShellHostViewModel.cs b/Source/NETworkManager/ViewModels/PowerShellHostViewModel.cs
index 2f3607b342..80527ee038 100644
--- a/Source/NETworkManager/ViewModels/PowerShellHostViewModel.cs
+++ b/Source/NETworkManager/ViewModels/PowerShellHostViewModel.cs
@@ -4,7 +4,6 @@
using NETworkManager.Controls;
using NETworkManager.Localization.Resources;
using NETworkManager.Models;
-using NETworkManager.Models.EventSystem;
using NETworkManager.Models.PowerShell;
using NETworkManager.Profiles;
using NETworkManager.Settings;
@@ -136,10 +135,6 @@ public PowerShellHostViewModel()
protected override ApplicationName ApplicationName => ApplicationName.PowerShell;
- protected override bool IsProfileEnabled(ProfileInfo profile) => profile.PowerShell_Enabled;
-
- protected override string GetSearchableField(ProfileInfo profile) => profile.PowerShell_Host;
-
///
/// Also mirrors the popup state into so the main window can suppress
/// global focus-stealing behavior (e.g. re-focusing the embedded terminal) while the filter popup is open.
@@ -233,7 +228,7 @@ private bool ConnectProfile_CanExecute(object obj)
private void ConnectProfileAction()
{
- ConnectProfile();
+ ConnectProfile(SelectedProfile);
}
public ICommand ConnectProfileExternalCommand => new RelayCommand(_ => ConnectProfileExternalAction());
@@ -264,7 +259,7 @@ private void ClearSearchAction()
private static void OpenSettingsAction()
{
- EventSystem.RedirectToSettings();
+ NavigationManager.OpenSettings();
}
#endregion
@@ -408,7 +403,8 @@ private Task Connect(string host = null)
// Add to history
// Note: The history can only be updated after the values have been read.
// Otherwise, in some cases, incorrect values are taken over.
- AddHostToHistory(instance.Host);
+ ListHelper.Modify(SettingsManager.Current.PowerShell_HostHistory, instance.Host,
+ SettingsManager.Current.General_HistoryListEntries);
Connect(sessionInfo);
}, _ =>
@@ -430,10 +426,13 @@ private Task Connect(string host = null)
return Application.Current.MainWindow.ShowChildWindowAsync(childWindow);
}
- private void ConnectProfile()
+ ///
+ /// Connects to the profile.
+ ///
+ /// Profile to connect to.
+ public void ConnectProfile(ProfileInfo profileInfo)
{
- Connect(PowerShellProfile.CreateSessionInfo(SelectedProfile),
- SelectedProfile.Name);
+ Connect(PowerShellProfile.CreateSessionInfo(profileInfo), profileInfo.Name);
}
private void ConnectProfileExternal()
@@ -468,16 +467,6 @@ public void AddTab(string host)
}
// Modify history list
- private static void AddHostToHistory(string host)
- {
- if (string.IsNullOrEmpty(host))
- return;
-
- SettingsManager.Current.PowerShell_HostHistory = new ObservableCollection(
- ListHelper.Modify(SettingsManager.Current.PowerShell_HostHistory.ToList(), host,
- SettingsManager.Current.General_HistoryListEntries));
- }
-
public void FocusEmbeddedWindow()
{
/* Don't continue if
diff --git a/Source/NETworkManager/ViewModels/ProfileHostViewModelBase.cs b/Source/NETworkManager/ViewModels/ProfileHostViewModelBase.cs
index dd58d3e331..1cf7e58328 100644
--- a/Source/NETworkManager/ViewModels/ProfileHostViewModelBase.cs
+++ b/Source/NETworkManager/ViewModels/ProfileHostViewModelBase.cs
@@ -36,13 +36,13 @@ public abstract class ProfileHostViewModelBase : ViewModelBase, IProfileHostView
///
/// Determines whether the given profile is enabled for this tool.
///
- protected abstract bool IsProfileEnabled(ProfileInfo profile);
+ private bool IsProfileEnabled(ProfileInfo profile) => profile.IsEnabled(ApplicationName);
///
- /// Gets the tool-specific field of the given profile that should also be matched against the search text
+ /// Gets the tool-specific field of the given profile (e.g. host) that is also matched against the search text
/// (in addition to the profile name).
///
- protected abstract string GetSearchableField(ProfileInfo profile);
+ private string GetSearchableField(ProfileInfo profile) => profile.GetTarget(ApplicationName) ?? string.Empty;
///
/// Gets the view for the profiles.
diff --git a/Source/NETworkManager/ViewModels/PuTTYHostViewModel.cs b/Source/NETworkManager/ViewModels/PuTTYHostViewModel.cs
index 9ffc59c06d..244e2fb7ad 100644
--- a/Source/NETworkManager/ViewModels/PuTTYHostViewModel.cs
+++ b/Source/NETworkManager/ViewModels/PuTTYHostViewModel.cs
@@ -4,7 +4,6 @@
using NETworkManager.Controls;
using NETworkManager.Localization.Resources;
using NETworkManager.Models;
-using NETworkManager.Models.EventSystem;
using NETworkManager.Models.PuTTY;
using NETworkManager.Profiles;
using NETworkManager.Settings;
@@ -136,10 +135,6 @@ public PuTTYHostViewModel()
protected override ApplicationName ApplicationName => ApplicationName.PuTTY;
- protected override bool IsProfileEnabled(ProfileInfo profile) => profile.PuTTY_Enabled;
-
- protected override string GetSearchableField(ProfileInfo profile) => profile.PuTTY_HostOrSerialLine;
-
///
/// Also mirrors the popup state into so the main window can suppress
/// global focus-stealing behavior (e.g. re-focusing the embedded terminal) while the filter popup is open.
@@ -241,7 +236,7 @@ private bool ConnectProfile_CanExecute(object obj)
private void ConnectProfileAction()
{
- ConnectProfile();
+ ConnectProfile(SelectedProfile);
}
public ICommand ConnectProfileExternalCommand => new RelayCommand(_ => ConnectProfileExternalAction());
@@ -265,7 +260,7 @@ public ICommand TextBoxSearchLostFocusCommand
private static void OpenSettingsAction()
{
- EventSystem.RedirectToSettings();
+ NavigationManager.OpenSettings();
}
#endregion
@@ -334,13 +329,26 @@ private Task Connect(string host = null)
// Add to history
// Note: The history can only be updated after the values have been read.
// Otherwise, in some cases, incorrect values are taken over.
- AddHostToHistory(instance.Host);
- AddSerialLineToHistory(instance.SerialLine);
- AddPortToHistory(instance.Port);
- AddBaudToHistory(instance.Baud);
- AddUsernameToHistory(instance.Username);
- AddPrivateKeyToHistory(instance.PrivateKeyFile);
- AddProfileToHistory(instance.Profile);
+ ListHelper.Modify(SettingsManager.Current.PuTTY_HostHistory, instance.Host,
+ SettingsManager.Current.General_HistoryListEntries);
+ ListHelper.Modify(SettingsManager.Current.PuTTY_SerialLineHistory, instance.SerialLine,
+ SettingsManager.Current.General_HistoryListEntries);
+
+ // Port and baud are 0 if they are not set.
+ if (instance.Port > 0)
+ ListHelper.Modify(SettingsManager.Current.PuTTY_PortHistory, instance.Port.ToString(),
+ SettingsManager.Current.General_HistoryListEntries);
+
+ if (instance.Baud > 0)
+ ListHelper.Modify(SettingsManager.Current.PuTTY_BaudHistory, instance.Baud.ToString(),
+ SettingsManager.Current.General_HistoryListEntries);
+
+ ListHelper.Modify(SettingsManager.Current.PuTTY_UsernameHistory, instance.Username,
+ SettingsManager.Current.General_HistoryListEntries);
+ ListHelper.Modify(SettingsManager.Current.PuTTY_PrivateKeyFileHistory, instance.PrivateKeyFile,
+ SettingsManager.Current.General_HistoryListEntries);
+ ListHelper.Modify(SettingsManager.Current.PuTTY_ProfileHistory, instance.Profile,
+ SettingsManager.Current.General_HistoryListEntries);
Connect(sessionInfo);
}, _ =>
@@ -362,9 +370,13 @@ private Task Connect(string host = null)
return Application.Current.MainWindow.ShowChildWindowAsync(childWindow);
}
- private void ConnectProfile()
+ ///
+ /// Connects to the profile.
+ ///
+ /// Profile to connect to.
+ public void ConnectProfile(ProfileInfo profileInfo)
{
- Connect(NETworkManager.Profiles.Application.PuTTY.CreateSessionInfo(SelectedProfile), SelectedProfile.Name);
+ Connect(NETworkManager.Profiles.Application.PuTTY.CreateSessionInfo(profileInfo), profileInfo.Name);
}
private void ConnectProfileExternal()
@@ -403,76 +415,6 @@ public void AddTab(string host)
}
// Modify history list
- private static void AddHostToHistory(string host)
- {
- if (string.IsNullOrEmpty(host))
- return;
-
- SettingsManager.Current.PuTTY_HostHistory = new ObservableCollection(
- ListHelper.Modify(SettingsManager.Current.PuTTY_HostHistory.ToList(), host,
- SettingsManager.Current.General_HistoryListEntries));
- }
-
- private static void AddSerialLineToHistory(string serialLine)
- {
- if (string.IsNullOrEmpty(serialLine))
- return;
-
- SettingsManager.Current.PuTTY_SerialLineHistory = new ObservableCollection(
- ListHelper.Modify(SettingsManager.Current.PuTTY_SerialLineHistory.ToList(), serialLine,
- SettingsManager.Current.General_HistoryListEntries));
- }
-
- private static void AddPortToHistory(int port)
- {
- if (port == 0)
- return;
-
- SettingsManager.Current.PuTTY_PortHistory = new ObservableCollection(
- ListHelper.Modify(SettingsManager.Current.PuTTY_PortHistory.ToList(), port.ToString(),
- SettingsManager.Current.General_HistoryListEntries));
- }
-
- private static void AddBaudToHistory(int baud)
- {
- if (baud == 0)
- return;
-
- SettingsManager.Current.PuTTY_BaudHistory = new ObservableCollection(
- ListHelper.Modify(SettingsManager.Current.PuTTY_BaudHistory.ToList(), baud.ToString(),
- SettingsManager.Current.General_HistoryListEntries));
- }
-
- private static void AddUsernameToHistory(string username)
- {
- if (string.IsNullOrEmpty(username))
- return;
-
- SettingsManager.Current.PuTTY_UsernameHistory = new ObservableCollection(
- ListHelper.Modify(SettingsManager.Current.PuTTY_UsernameHistory.ToList(), username,
- SettingsManager.Current.General_HistoryListEntries));
- }
-
- private static void AddPrivateKeyToHistory(string privateKey)
- {
- if (string.IsNullOrEmpty(privateKey))
- return;
-
- SettingsManager.Current.PuTTY_PrivateKeyFileHistory = new ObservableCollection(
- ListHelper.Modify(SettingsManager.Current.PuTTY_PrivateKeyFileHistory.ToList(), privateKey,
- SettingsManager.Current.General_HistoryListEntries));
- }
-
- private static void AddProfileToHistory(string profile)
- {
- if (string.IsNullOrEmpty(profile))
- return;
-
- SettingsManager.Current.PuTTY_ProfileHistory = new ObservableCollection(
- ListHelper.Modify(SettingsManager.Current.PuTTY_ProfileHistory.ToList(), profile,
- SettingsManager.Current.General_HistoryListEntries));
- }
-
public void FocusEmbeddedWindow()
{
/* Don't continue if
diff --git a/Source/NETworkManager/ViewModels/RDAPHostViewModel.cs b/Source/NETworkManager/ViewModels/RDAPHostViewModel.cs
index 72fe499c95..84ccc2acfa 100644
--- a/Source/NETworkManager/ViewModels/RDAPHostViewModel.cs
+++ b/Source/NETworkManager/ViewModels/RDAPHostViewModel.cs
@@ -71,10 +71,6 @@ public RDAPHostViewModel()
protected override ApplicationName ApplicationName => ApplicationName.RDAP;
- protected override bool IsProfileEnabled(ProfileInfo profile) => profile.RDAP_Enabled;
-
- protected override string GetSearchableField(ProfileInfo profile) => profile.RDAP_Query;
-
#endregion
#region ICommand & Actions
@@ -95,7 +91,7 @@ private bool QueryProfile_CanExecute(object obj)
private void QueryProfileAction()
{
- AddTab(SelectedProfile.RDAP_Query, SelectedProfile.RDAP_QueryType);
+ AddTab(SelectedProfile);
}
public ItemActionCallback CloseItemCommand => CloseItemAction;
@@ -130,5 +126,14 @@ public void AddTab(string query = null, RDAPQueryType? queryType = null)
SelectedTabIndex = TabItems.Count - 1;
}
+ ///
+ /// Adds a new tab and starts the query of the profile.
+ ///
+ /// Profile to query.
+ public void AddTab(ProfileInfo profile)
+ {
+ AddTab(profile.RDAP_Query, profile.RDAP_QueryType);
+ }
+
#endregion
}
diff --git a/Source/NETworkManager/ViewModels/RDAPViewModel.cs b/Source/NETworkManager/ViewModels/RDAPViewModel.cs
index 5dfa44cb1d..33f052ebb6 100644
--- a/Source/NETworkManager/ViewModels/RDAPViewModel.cs
+++ b/Source/NETworkManager/ViewModels/RDAPViewModel.cs
@@ -4,7 +4,6 @@
using NETworkManager.Controls;
using NETworkManager.Localization.Resources;
using NETworkManager.Models;
-using NETworkManager.Models.EventSystem;
using NETworkManager.Models.Export;
using NETworkManager.Models.RDAP;
using NETworkManager.Settings;
@@ -263,7 +262,7 @@ private void QueryAction()
private void OpenInWhoisAction()
{
- EventSystem.RedirectToApplication(ApplicationName.Whois, _whoisDomain);
+ NavigationManager.Open(new ApplicationRequest(ApplicationName.Whois) { Target = _whoisDomain });
}
///
@@ -350,7 +349,8 @@ private async Task RunQuery()
if (messages.Count > 0)
ShowStatusMessage(string.Join(Environment.NewLine, messages));
- AddQueryToHistory(Query);
+ ListHelper.Modify(SettingsManager.Current.RDAP_QueryHistory, Query,
+ SettingsManager.Current.General_HistoryListEntries);
}
catch (OperationCanceledException)
{
@@ -447,20 +447,6 @@ public void OnClose()
ConfigurationManager.Current.RDAPTabCount--;
}
- private void AddQueryToHistory(string query)
- {
- // Create the new list
- var list = ListHelper.Modify(SettingsManager.Current.RDAP_QueryHistory.ToList(), query,
- SettingsManager.Current.General_HistoryListEntries);
-
- // Clear the old items
- SettingsManager.Current.RDAP_QueryHistory.Clear();
- OnPropertyChanged(nameof(Query)); // Raise property changed again, after the collection has been cleared
-
- // Fill with the new items
- list.ForEach(x => SettingsManager.Current.RDAP_QueryHistory.Add(x));
- }
-
///
/// Gets the content for the export. JSON contains the responses exactly as sent by the server(s), as an array
/// if the registrar referral was followed.
diff --git a/Source/NETworkManager/ViewModels/RemoteDesktopHostViewModel.cs b/Source/NETworkManager/ViewModels/RemoteDesktopHostViewModel.cs
index 1de5001b77..27b990ecb7 100644
--- a/Source/NETworkManager/ViewModels/RemoteDesktopHostViewModel.cs
+++ b/Source/NETworkManager/ViewModels/RemoteDesktopHostViewModel.cs
@@ -74,10 +74,6 @@ public RemoteDesktopHostViewModel()
protected override ApplicationName ApplicationName => ApplicationName.RemoteDesktop;
- protected override bool IsProfileEnabled(ProfileInfo profile) => profile.RemoteDesktop_Enabled;
-
- protected override string GetSearchableField(ProfileInfo profile) => profile.RemoteDesktop_Host;
-
public override void OnProfileManagerDialogOpen()
{
ConfigurationManager.OnDialogOpen();
@@ -213,7 +209,7 @@ private bool ConnectProfile_CanExecute(object obj)
private void ConnectProfileAction()
{
- ConnectProfile();
+ ConnectProfile(SelectedProfile);
}
public ICommand ConnectProfileAsCommand => new RelayCommand(_ => ConnectProfileAsAction());
@@ -289,7 +285,8 @@ private Task Connect(string host = null)
// Add to history
// Note: The history can only be updated after the values have been read.
// Otherwise, in some cases, incorrect values are taken over.
- AddHostToHistory(instance.Host);
+ ListHelper.Modify(SettingsManager.Current.RemoteDesktop_HostHistory, instance.Host,
+ SettingsManager.Current.General_HistoryListEntries);
Connect(sessionInfo);
}, _ =>
@@ -315,10 +312,12 @@ private Task Connect(string host = null)
}
// Connect via Profile
- private void ConnectProfile()
+ ///
+ /// Connects to the profile.
+ ///
+ /// Profile to connect to.
+ public void ConnectProfile(ProfileInfo profileInfo)
{
- var profileInfo = SelectedProfile;
-
var sessionInfo = RemoteDesktop.CreateSessionInfo(profileInfo);
Connect(sessionInfo, profileInfo.Name);
@@ -394,15 +393,5 @@ public void AddTab(string host)
}
// Modify history list
- private static void AddHostToHistory(string host)
- {
- if (string.IsNullOrEmpty(host))
- return;
-
- SettingsManager.Current.RemoteDesktop_HostHistory = new ObservableCollection(
- ListHelper.Modify(SettingsManager.Current.RemoteDesktop_HostHistory.ToList(), host,
- SettingsManager.Current.General_HistoryListEntries));
- }
-
#endregion
}
diff --git a/Source/NETworkManager/ViewModels/RunCommandViewModel.cs b/Source/NETworkManager/ViewModels/RunCommandViewModel.cs
new file mode 100644
index 0000000000..d3868fd400
--- /dev/null
+++ b/Source/NETworkManager/ViewModels/RunCommandViewModel.cs
@@ -0,0 +1,485 @@
+using NETworkManager.Localization.Resources;
+using NETworkManager.Models.RunCommand;
+using NETworkManager.Profiles;
+using NETworkManager.Settings;
+using NETworkManager.Utilities;
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Windows.Input;
+
+namespace NETworkManager.ViewModels;
+
+///
+/// View model of the run command dialog: type a command (e.g. "ping 10.0.0.1"), select a suggestion and run it.
+///
+public class RunCommandViewModel : ViewModelBase
+{
+ ///
+ /// Maximum number of suggestions shown for a target or argument (profiles, history, types). The commands are
+ /// always shown completely.
+ ///
+ private const int MaxSuggestions = 10;
+
+ ///
+ /// Maximum number of recent commands shown with an empty input (before the commands).
+ ///
+ private const int MaxHistorySuggestions = 5;
+
+ ///
+ /// Maximum number of recent commands of a command shown before its profiles.
+ ///
+ private const int MaxCommandHistorySuggestions = 3;
+
+ private readonly IReadOnlyList _commands;
+ private readonly Action _runHandler;
+
+ ///
+ /// Recent commands with their command (parsed once, the history doesn't change while the dialog is open).
+ ///
+ private readonly List<(string Text, RunCommandDefinition Command)> _history;
+
+ ///
+ /// Names (command and aliases) and translated name per command.
+ ///
+ private readonly List<(RunCommandDefinition Command, List Names, string TranslatedName)> _commandNames;
+
+ ///
+ /// Profiles per command (loaded once per command while the dialog is open).
+ ///
+ private readonly Dictionary> _profiles = [];
+
+ private RunCommandParseResult _result;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Called with the request to run (, ).
+ /// Called when the dialog is canceled.
+ public RunCommandViewModel(Action runHandler, Action cancelHandler)
+ {
+ _commands = RunCommandManager.GetCommands();
+ _runHandler = runHandler;
+
+ _commandNames =
+ [
+ .. _commands.Select(x => (x, new List { x.Command }.Concat(x.Aliases).ToList(),
+ RunCommandManager.GetTranslatedName(x)))
+ ];
+
+ // Entries of commands that are not available anymore (e.g. a hidden application) are not shown.
+ _history =
+ [
+ .. SettingsManager.Current.RunCommand_History.Select(x => (x, RunCommandParser.Parse(x, _commands).Command))
+ .Where(x => x.Command != null)
+ ];
+
+ CancelCommand = new RelayCommand(_ => cancelHandler(this));
+
+ UpdateSuggestions();
+ }
+
+ #region Properties
+
+ public ICommand CancelCommand { get; }
+
+ ///
+ /// Input of the user (e.g. "dns borntoberoot.net aaaa").
+ ///
+ public string Input
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+
+ field = value;
+ OnPropertyChanged();
+
+ ErrorMessage = null;
+ UpdateSuggestions();
+ }
+ } = string.Empty;
+
+ public ObservableCollection Suggestions { get; } = [];
+
+ public RunCommandSuggestion SelectedSuggestion
+ {
+ get;
+ set
+ {
+ if (value == field)
+ return;
+
+ field = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Syntax of the recognized command (e.g. "dnslookup <hosts> [server] [type] | @<profile>").
+ ///
+ public string Hint
+ {
+ get;
+ private set
+ {
+ if (value == field)
+ return;
+
+ field = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Error of the last run (e.g. invalid host), null if there is none.
+ ///
+ public string ErrorMessage
+ {
+ get;
+ private set
+ {
+ if (value == field)
+ return;
+
+ field = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Request to run (set before the run handler is called), null for the settings.
+ ///
+ public ApplicationRequest Request { get; private set; }
+
+ ///
+ /// The settings should be opened (set before the run handler is called).
+ ///
+ public bool IsSettings { get; private set; }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ /// Selects the next suggestion (Down).
+ ///
+ public void SelectNext()
+ {
+ Select(1);
+ }
+
+ ///
+ /// Selects the previous suggestion (Up).
+ ///
+ public void SelectPrevious()
+ {
+ Select(-1);
+ }
+
+ private void Select(int offset)
+ {
+ if (Suggestions.Count == 0)
+ return;
+
+ var index = SelectedSuggestion == null ? -1 : Suggestions.IndexOf(SelectedSuggestion);
+
+ SelectedSuggestion = Suggestions[Math.Clamp(index + offset, 0, Suggestions.Count - 1)];
+ }
+
+ ///
+ /// Inserts the selected suggestion into the input (Tab).
+ ///
+ public void Complete()
+ {
+ if (SelectedSuggestion == null || SelectedSuggestion.Kind == RunCommandSuggestionKind.Run)
+ return;
+
+ Input = SelectedSuggestion.CompletionText;
+ }
+
+ ///
+ /// Runs the selected suggestion, or the input if none is selected (Enter).
+ ///
+ public void Run()
+ {
+ var suggestion = SelectedSuggestion;
+
+ var text = suggestion?.CompletionText ?? Input;
+ var result = RunCommandParser.Parse(text, _commands);
+
+ var error = RunCommandManager.TryCreateRequest(result, suggestion?.Profile, out var request);
+
+ if (error != null)
+ {
+ // Show the text of a failed suggestion (e.g. a recent command with an ambiguous profile name), so it can be
+ // corrected or completed with the suggestions.
+ if (suggestion != null && suggestion.Kind != RunCommandSuggestionKind.Run)
+ Input = text;
+
+ ErrorMessage = error;
+
+ return;
+ }
+
+ Request = request;
+ IsSettings = result.Command.Type == RunCommandType.Setting;
+
+ // Commands are case-insensitive, so "Ping 10.0.0.1" and "ping 10.0.0.1" are the same entry.
+ ListHelper.Modify(SettingsManager.Current.RunCommand_History, text.Trim(),
+ SettingsManager.Current.General_HistoryListEntries, StringComparer.OrdinalIgnoreCase);
+
+ _runHandler(this);
+ }
+
+ ///
+ /// Updates the suggestions and the hint for the input.
+ ///
+ private void UpdateSuggestions()
+ {
+ _result = RunCommandParser.Parse(Input, _commands);
+
+ Hint = _result.Command == null
+ ? null
+ : $"{_result.Command.Command} {_result.Command.Syntax}".TrimEnd();
+
+ IEnumerable suggestions = _result.Position switch
+ {
+ RunCommandPosition.Command => GetCommandSuggestions(),
+ RunCommandPosition.Target when _result.ProfileName != null => GetProfileSuggestions(),
+ RunCommandPosition.Target =>
+ [
+ GetRunSuggestion(), .. GetHistorySuggestions().Take(MaxCommandHistorySuggestions),
+ .. GetProfileSuggestions()
+ ],
+ _ =>
+ [
+ GetRunSuggestion(), .. GetTypeSuggestions(), .. GetDNSServerSuggestions(),
+ .. GetHistorySuggestions()
+ ]
+ };
+
+ if (_result.Position != RunCommandPosition.Command)
+ suggestions = suggestions.Take(MaxSuggestions);
+
+ Suggestions.Clear();
+
+ foreach (var suggestion in suggestions)
+ Suggestions.Add(suggestion);
+
+ SelectedSuggestion = GetPreselectedSuggestion();
+ }
+
+ ///
+ /// Gets the suggestion that Enter runs without selecting one, so Enter runs what was typed:
+ /// - After the command: the input as typed (first suggestion).
+ /// - While typing the command: the best matching command, but not if the input is empty or an unknown command has
+ /// arguments (e.g. a typo like "pin 10.0.0.1"), which would drop the input.
+ /// - While typing a profile ("@"): the profile, only if exactly one matches. Otherwise, the typed name is used,
+ /// which must be unique (e.g. two profiles with the same name in different groups).
+ ///
+ private RunCommandSuggestion GetPreselectedSuggestion()
+ {
+ return _result.Position switch
+ {
+ RunCommandPosition.Command when string.IsNullOrWhiteSpace(Input) ||
+ _result.Command == null && GetInputAfterCommand().Length > 0 => null,
+ RunCommandPosition.Target when _result.ProfileName != null => Suggestions.Count == 1 ? Suggestions[0] : null,
+ _ => Suggestions.FirstOrDefault()
+ };
+ }
+
+ ///
+ /// Gets the input after the first word (the command), e.g. "10.0.0.1" for "pin 10.0.0.1".
+ ///
+ private string GetInputAfterCommand()
+ {
+ var input = Input.TrimStart();
+ var index = input.IndexOfAny([' ', '\t']);
+
+ return index < 0 ? string.Empty : input[index..].Trim();
+ }
+
+ ///
+ /// Suggestion to run the input as typed.
+ ///
+ private RunCommandSuggestion GetRunSuggestion()
+ {
+ return new RunCommandSuggestion
+ {
+ Kind = RunCommandSuggestionKind.Run,
+ Application = _result.Command.Application,
+ Title = Input.Trim(),
+ Description = RunCommandManager.GetTranslatedName(_result.Command),
+ CompletionText = Input
+ };
+ }
+
+ ///
+ /// Recent commands (empty input) and the commands that match the typed command.
+ ///
+ private IEnumerable GetCommandSuggestions()
+ {
+ var text = _result.CurrentWord;
+
+ var suggestions = new List();
+
+ if (string.IsNullOrEmpty(text))
+ suggestions.AddRange(GetHistorySuggestions().Take(MaxHistorySuggestions));
+
+ // Completing the command keeps the arguments (e.g. "pin 10.0.0.1" → "ping 10.0.0.1").
+ var arguments = GetInputAfterCommand();
+
+ var commands = new List<(int Rank, RunCommandSuggestion Suggestion)>();
+
+ foreach (var (command, names, translatedName) in _commandNames)
+ {
+ // Complete with the command or alias that was typed, or the shortest one that starts with the text (e.g.
+ // "pin" → "ping" instead of "pingmonitor").
+ var exactName = names.FirstOrDefault(x => string.Equals(x, text, StringComparison.OrdinalIgnoreCase));
+ var name = exactName ?? names.Where(x => x.StartsWith(text, StringComparison.OrdinalIgnoreCase))
+ .OrderBy(x => x.Length).FirstOrDefault();
+
+ if (name == null && !translatedName.Contains(text, StringComparison.OrdinalIgnoreCase))
+ continue;
+
+ // Exact command or alias first, then commands that start with the text, then translated names that contain
+ // it (e.g. "lookup" before "DNS Lookup").
+ var rank = exactName != null ? 0 : name != null ? 1 : 2;
+
+ commands.Add((rank, new RunCommandSuggestion
+ {
+ Kind = RunCommandSuggestionKind.Command,
+ Application = command.Application,
+ Title = translatedName,
+ Description = string.Join(", ", names),
+ Info = command.Syntax,
+ CompletionText = (name ?? command.Command) + " " + arguments
+ }));
+ }
+
+ suggestions.AddRange(commands.OrderBy(x => x.Rank).Select(x => x.Suggestion));
+
+ return suggestions;
+ }
+
+ ///
+ /// Profiles of the command that match the typed profile name (or target).
+ ///
+ private IEnumerable GetProfileSuggestions()
+ {
+ var text = _result.ProfileName ?? _result.CurrentWord;
+ var application = _result.Command.Application;
+
+ // Exact name first, then names that start with the text, then names and targets (e.g. host) that contain it.
+ static int? GetRank(ProfileInfo profile, string target, string text)
+ {
+ if (string.IsNullOrEmpty(text) || string.Equals(profile.Name, text, StringComparison.OrdinalIgnoreCase))
+ return 0;
+
+ if (profile.Name.StartsWith(text, StringComparison.OrdinalIgnoreCase))
+ return 1;
+
+ if (profile.Name.Contains(text, StringComparison.OrdinalIgnoreCase) ||
+ (target?.Contains(text, StringComparison.OrdinalIgnoreCase) ?? false))
+ return 2;
+
+ return null;
+ }
+
+ if (!_profiles.TryGetValue(_result.Command, out var profiles))
+ {
+ profiles = [.. RunCommandManager.GetProfiles(_result.Command)];
+ _profiles[_result.Command] = profiles;
+ }
+
+ return profiles
+ .Select(x => (Profile: x, Target: x.GetTarget(application)))
+ .Select(x => (x.Profile, x.Target, Rank: GetRank(x.Profile, x.Target, text)))
+ .Where(x => x.Rank != null)
+ .OrderBy(x => x.Rank)
+ .Select(x => new RunCommandSuggestion
+ {
+ Kind = RunCommandSuggestionKind.Profile,
+ Application = application,
+ Title = x.Profile.Name,
+ // The target of the application (e.g. the RDP host), which can differ from the general host.
+ Description = string.IsNullOrEmpty(x.Target) ? x.Profile.Group : $"{x.Profile.Group} · {x.Target}",
+ Info = "@" + x.Profile.Name,
+ CompletionText = _result.InputBeforeCurrentWord + "@" + x.Profile.Name,
+ Profile = x.Profile
+ });
+ }
+
+ ///
+ /// Type keywords of the command that start with the typed word.
+ ///
+ private IEnumerable GetTypeSuggestions()
+ {
+ // A complete type was already entered (and is not being typed).
+ if (_result.Type != null && !string.Equals(_result.Type, _result.CurrentWord, StringComparison.OrdinalIgnoreCase))
+ return [];
+
+ return _result.Command.Types
+ .Where(x => x.StartsWith(_result.CurrentWord, StringComparison.OrdinalIgnoreCase))
+ .Select(x => new RunCommandSuggestion
+ {
+ Kind = RunCommandSuggestionKind.Type,
+ Application = _result.Command.Application,
+ Title = x.ToUpperInvariant(),
+ Description = Strings.Type,
+ CompletionText = _result.InputBeforeCurrentWord + x + " "
+ });
+ }
+
+ ///
+ /// DNS servers configured in the DNS lookup (e.g. "Cloudflare") that contain the typed word, if no DNS server was
+ /// entered yet. Names with spaces are inserted in quotes.
+ ///
+ private IEnumerable GetDNSServerSuggestions()
+ {
+ if (_result.Command.Argument != RunCommandArgument.DNSServer ||
+ _result.Argument != null && !string.Equals(_result.Argument, _result.CurrentWord))
+ return [];
+
+ return RunCommandManager.GetDNSServers()
+ .Where(x => x.Contains(_result.CurrentWord, StringComparison.OrdinalIgnoreCase))
+ .Select(x => new RunCommandSuggestion
+ {
+ Kind = RunCommandSuggestionKind.DNSServer,
+ Application = _result.Command.Application,
+ Title = x,
+ Description = Strings.DNSServer,
+ CompletionText = _result.InputBeforeCurrentWord + (x.Contains(' ') ? $"\"{x}\"" : x) + " "
+ });
+ }
+
+ ///
+ /// Recent commands. With a command, only its entries that contain the input.
+ ///
+ private IEnumerable GetHistorySuggestions()
+ {
+ var input = Input.Trim();
+
+ return _history
+ .Where(x => _result.Command == null && string.IsNullOrEmpty(input) ||
+ _result.Command != null &&
+ x.Command == _result.Command &&
+ x.Text.Contains(_result.CurrentWord, StringComparison.OrdinalIgnoreCase) &&
+ !string.Equals(x.Text, input, StringComparison.OrdinalIgnoreCase))
+ .Select(x => new RunCommandSuggestion
+ {
+ Kind = RunCommandSuggestionKind.History,
+ Application = x.Command?.Application ?? Models.ApplicationName.None,
+ Title = x.Text,
+ Description = x.Command == null ? null : RunCommandManager.GetTranslatedName(x.Command),
+ Info = Strings.Recent,
+ CompletionText = x.Text
+ });
+ }
+
+ #endregion
+}
diff --git a/Source/NETworkManager/ViewModels/SNMPHostViewModel.cs b/Source/NETworkManager/ViewModels/SNMPHostViewModel.cs
index f73e4a0601..4d215d8002 100644
--- a/Source/NETworkManager/ViewModels/SNMPHostViewModel.cs
+++ b/Source/NETworkManager/ViewModels/SNMPHostViewModel.cs
@@ -68,10 +68,6 @@ public SNMPHostViewModel()
protected override ApplicationName ApplicationName => ApplicationName.SNMP;
- protected override bool IsProfileEnabled(ProfileInfo profile) => profile.SNMP_Enabled;
-
- protected override string GetSearchableField(ProfileInfo profile) => profile.SNMP_Host;
-
#endregion
#region ICommand & Actions
@@ -132,7 +128,7 @@ public void AddTab(string host)
AddTab(sessionInfo);
}
- private void AddTab(ProfileInfo profile)
+ public void AddTab(ProfileInfo profile)
{
var sessionInfo = SNMP.CreateSessionInfo(profile);
diff --git a/Source/NETworkManager/ViewModels/SNMPViewModel.cs b/Source/NETworkManager/ViewModels/SNMPViewModel.cs
index 8119896d37..ac69b07627 100644
--- a/Source/NETworkManager/ViewModels/SNMPViewModel.cs
+++ b/Source/NETworkManager/ViewModels/SNMPViewModel.cs
@@ -581,8 +581,10 @@ await DNSClientHelper.ResolveAorAaaaAsync(Host,
_cancellationTokenSource.CancelAfter(SettingsManager.Current.SNMP_Timeout);
// Add to history...
- AddHostToHistory(Host);
- AddOidToHistory(Oid);
+ ListHelper.Modify(SettingsManager.Current.SNMP_HostHistory, Host,
+ SettingsManager.Current.General_HistoryListEntries);
+ ListHelper.Modify(SettingsManager.Current.SNMP_OidHistory, Oid,
+ SettingsManager.Current.General_HistoryListEntries);
}
private void StopWork()
@@ -633,34 +635,6 @@ private async Task OpenOIDProfileSelection()
await window.ShowChildWindowAsync(childWindow);
}
- private void AddHostToHistory(string host)
- {
- // Create the new list
- var list = ListHelper.Modify(SettingsManager.Current.SNMP_HostHistory.ToList(), host,
- SettingsManager.Current.General_HistoryListEntries);
-
- // Clear the old items
- SettingsManager.Current.SNMP_HostHistory.Clear();
- OnPropertyChanged(nameof(Host)); // Raise property changed again, after the collection has been cleared
-
- // Fill with the new items
- list.ForEach(x => SettingsManager.Current.SNMP_HostHistory.Add(x));
- }
-
- private void AddOidToHistory(string oid)
- {
- // Create the new list
- var list = ListHelper.Modify(SettingsManager.Current.SNMP_OidHistory.ToList(), oid,
- SettingsManager.Current.General_HistoryListEntries);
-
- // Clear the old items
- SettingsManager.Current.SNMP_OidHistory.Clear();
- OnPropertyChanged(nameof(Oid)); // Raise property changed again, after the collection has been cleared
-
- // Fill with the new items
- list.ForEach(x => SettingsManager.Current.SNMP_OidHistory.Add(x));
- }
-
private Task Export()
{
var window = Application.Current.Windows.OfType().FirstOrDefault(x => x.IsActive);
diff --git a/Source/NETworkManager/ViewModels/SubnetCalculatorCalculatorViewModel.cs b/Source/NETworkManager/ViewModels/SubnetCalculatorCalculatorViewModel.cs
index 947254e44e..6e9c117ab9 100644
--- a/Source/NETworkManager/ViewModels/SubnetCalculatorCalculatorViewModel.cs
+++ b/Source/NETworkManager/ViewModels/SubnetCalculatorCalculatorViewModel.cs
@@ -112,23 +112,10 @@ private void Calculate()
IsResultVisible = true;
- AddSubnetToHistory(subnet);
-
- IsRunning = false;
- }
-
- private void AddSubnetToHistory(string subnet)
- {
- // Create the new list
- var list = ListHelper.Modify(SettingsManager.Current.SubnetCalculator_Calculator_SubnetHistory.ToList(), subnet,
+ ListHelper.Modify(SettingsManager.Current.SubnetCalculator_Calculator_SubnetHistory, subnet,
SettingsManager.Current.General_HistoryListEntries);
- // Clear the old items
- SettingsManager.Current.SubnetCalculator_Calculator_SubnetHistory.Clear();
- OnPropertyChanged(nameof(Subnet)); // Raise property changed again, after the collection has been cleared
-
- // Fill with the new items
- list.ForEach(x => SettingsManager.Current.SubnetCalculator_Calculator_SubnetHistory.Add(x));
+ IsRunning = false;
}
#endregion
diff --git a/Source/NETworkManager/ViewModels/SubnetCalculatorSubnettingViewModel.cs b/Source/NETworkManager/ViewModels/SubnetCalculatorSubnettingViewModel.cs
index 45dcc67af1..8f4da94fba 100644
--- a/Source/NETworkManager/ViewModels/SubnetCalculatorSubnettingViewModel.cs
+++ b/Source/NETworkManager/ViewModels/SubnetCalculatorSubnettingViewModel.cs
@@ -253,38 +253,12 @@ await Task.Run(() =>
IsResultVisible = true;
- AddSubnetToHistory(subnet);
- AddNewSubnetmaskOrCidrToHistory(newSubnetmaskOrCidr);
-
- IsRunning = false;
- }
-
- private void AddSubnetToHistory(string subnet)
- {
- // Create the new list
- var list = ListHelper.Modify(SettingsManager.Current.SubnetCalculator_Subnetting_SubnetHistory.ToList(), subnet,
+ ListHelper.Modify(SettingsManager.Current.SubnetCalculator_Subnetting_SubnetHistory, subnet,
+ SettingsManager.Current.General_HistoryListEntries);
+ ListHelper.Modify(SettingsManager.Current.SubnetCalculator_Subnetting_NewSubnetmaskHistory, newSubnetmaskOrCidr,
SettingsManager.Current.General_HistoryListEntries);
- // Clear the old items
- SettingsManager.Current.SubnetCalculator_Subnetting_SubnetHistory.Clear();
- OnPropertyChanged(nameof(Subnet)); // Raise property changed again, after the collection has been cleared
-
- // Fill with the new items
- list.ForEach(x => SettingsManager.Current.SubnetCalculator_Subnetting_SubnetHistory.Add(x));
- }
-
- private void AddNewSubnetmaskOrCidrToHistory(string newSubnetmaskOrCidr)
- {
- // Create the new list
- var list = ListHelper.Modify(SettingsManager.Current.SubnetCalculator_Subnetting_NewSubnetmaskHistory.ToList(),
- newSubnetmaskOrCidr, SettingsManager.Current.General_HistoryListEntries);
-
- // Clear the old items
- SettingsManager.Current.SubnetCalculator_Subnetting_NewSubnetmaskHistory.Clear();
- OnPropertyChanged(nameof(NewSubnetmask)); // Raise property changed again, after the collection has been cleared
-
- // Fill with the new items
- list.ForEach(x => SettingsManager.Current.SubnetCalculator_Subnetting_NewSubnetmaskHistory.Add(x));
+ IsRunning = false;
}
public void OnShutdown()
diff --git a/Source/NETworkManager/ViewModels/SubnetCalculatorWideSubnetViewModel.cs b/Source/NETworkManager/ViewModels/SubnetCalculatorWideSubnetViewModel.cs
index c351a0dbad..e743d11ca4 100644
--- a/Source/NETworkManager/ViewModels/SubnetCalculatorWideSubnetViewModel.cs
+++ b/Source/NETworkManager/ViewModels/SubnetCalculatorWideSubnetViewModel.cs
@@ -134,38 +134,12 @@ private void Calculate()
IsResultVisible = true;
- AddSubnet1ToHistory(subnet1);
- AddSubnet2ToHistory(subnet2);
-
- IsRunning = false;
- }
-
- private void AddSubnet1ToHistory(string subnet)
- {
- // Create the new list
- var list = ListHelper.Modify(SettingsManager.Current.SubnetCalculator_WideSubnet_Subnet1.ToList(), subnet,
+ ListHelper.Modify(SettingsManager.Current.SubnetCalculator_WideSubnet_Subnet1, subnet1,
SettingsManager.Current.General_HistoryListEntries);
-
- // Clear the old items
- SettingsManager.Current.SubnetCalculator_WideSubnet_Subnet1.Clear();
- OnPropertyChanged(nameof(Subnet1)); // Raise property changed again, after the collection has been cleared
-
- // Fill with the new items
- list.ForEach(x => SettingsManager.Current.SubnetCalculator_WideSubnet_Subnet1.Add(x));
- }
-
- private void AddSubnet2ToHistory(string subnet)
- {
- // Create the new list
- var list = ListHelper.Modify(SettingsManager.Current.SubnetCalculator_WideSubnet_Subnet2.ToList(), subnet,
+ ListHelper.Modify(SettingsManager.Current.SubnetCalculator_WideSubnet_Subnet2, subnet2,
SettingsManager.Current.General_HistoryListEntries);
- // Clear the old items
- SettingsManager.Current.SubnetCalculator_WideSubnet_Subnet2.Clear();
- OnPropertyChanged(nameof(Subnet2)); // Raise property changed again, after the collection has been cleared
-
- // Fill with the new items
- list.ForEach(x => SettingsManager.Current.SubnetCalculator_WideSubnet_Subnet2.Add(x));
+ IsRunning = false;
}
public void OnShutdown()
diff --git a/Source/NETworkManager/ViewModels/TigerVNCHostViewModel.cs b/Source/NETworkManager/ViewModels/TigerVNCHostViewModel.cs
index e9f47e675d..0656b89157 100644
--- a/Source/NETworkManager/ViewModels/TigerVNCHostViewModel.cs
+++ b/Source/NETworkManager/ViewModels/TigerVNCHostViewModel.cs
@@ -3,7 +3,6 @@
using NETworkManager.Controls;
using NETworkManager.Localization.Resources;
using NETworkManager.Models;
-using NETworkManager.Models.EventSystem;
using NETworkManager.Models.TigerVNC;
using NETworkManager.Profiles;
using NETworkManager.Settings;
@@ -93,10 +92,6 @@ public TigerVNCHostViewModel()
protected override ApplicationName ApplicationName => ApplicationName.TigerVNC;
- protected override bool IsProfileEnabled(ProfileInfo profile) => profile.TigerVNC_Enabled;
-
- protected override string GetSearchableField(ProfileInfo profile) => profile.TigerVNC_Host;
-
public override void OnProfileManagerDialogOpen()
{
ConfigurationManager.OnDialogOpen();
@@ -150,7 +145,7 @@ private bool ConnectProfile_CanExecute(object obj)
private void ConnectProfileAction()
{
- ConnectProfile();
+ ConnectProfile(SelectedProfile);
}
public ICommand ConnectProfileExternalCommand => new RelayCommand(_ => ConnectProfileExternalAction());
@@ -164,7 +159,7 @@ private void ConnectProfileExternalAction()
private static void OpenSettingsAction()
{
- EventSystem.RedirectToSettings();
+ NavigationManager.OpenSettings();
}
#endregion
@@ -198,8 +193,10 @@ private Task Connect(string host = null)
// Add to history
// Note: The history can only be updated after the values have been read.
// Otherwise, in some cases, incorrect values are taken over.
- AddHostToHistory(instance.Host);
- AddPortToHistory(instance.Port);
+ ListHelper.Modify(SettingsManager.Current.TigerVNC_HostHistory, instance.Host,
+ SettingsManager.Current.General_HistoryListEntries);
+ ListHelper.Modify(SettingsManager.Current.TigerVNC_PortHistory, instance.Port,
+ SettingsManager.Current.General_HistoryListEntries);
// Connect
Connect(sessionInfo);
@@ -223,9 +220,13 @@ private Task Connect(string host = null)
return Application.Current.MainWindow.ShowChildWindowAsync(childWindow);
}
- private void ConnectProfile()
+ ///
+ /// Connects to the profile.
+ ///
+ /// Profile to connect to.
+ public void ConnectProfile(ProfileInfo profileInfo)
{
- Connect(TigerVNC.CreateSessionInfo(SelectedProfile), SelectedProfile.Name);
+ Connect(TigerVNC.CreateSessionInfo(profileInfo), profileInfo.Name);
}
private void ConnectProfileExternal()
@@ -256,26 +257,6 @@ public void AddTab(string host)
}
// Modify history list
- private static void AddHostToHistory(string host)
- {
- if (string.IsNullOrEmpty(host))
- return;
-
- SettingsManager.Current.TigerVNC_HostHistory = new ObservableCollection(
- ListHelper.Modify(SettingsManager.Current.TigerVNC_HostHistory.ToList(), host,
- SettingsManager.Current.General_HistoryListEntries));
- }
-
- private static void AddPortToHistory(int port)
- {
- if (port == 0)
- return;
-
- SettingsManager.Current.TigerVNC_PortHistory = new ObservableCollection(
- ListHelper.Modify(SettingsManager.Current.TigerVNC_PortHistory.ToList(), port,
- SettingsManager.Current.General_HistoryListEntries));
- }
-
#endregion
#region Event
diff --git a/Source/NETworkManager/ViewModels/TracerouteHostViewModel.cs b/Source/NETworkManager/ViewModels/TracerouteHostViewModel.cs
index 40407b6d5f..f0ec9dacd8 100644
--- a/Source/NETworkManager/ViewModels/TracerouteHostViewModel.cs
+++ b/Source/NETworkManager/ViewModels/TracerouteHostViewModel.cs
@@ -70,10 +70,6 @@ public TracerouteHostViewModel()
protected override ApplicationName ApplicationName => ApplicationName.Traceroute;
- protected override bool IsProfileEnabled(ProfileInfo profile) => profile.Traceroute_Enabled;
-
- protected override string GetSearchableField(ProfileInfo profile) => profile.Traceroute_Host;
-
#endregion
#region ICommand & Actions
diff --git a/Source/NETworkManager/ViewModels/TracerouteViewModel.cs b/Source/NETworkManager/ViewModels/TracerouteViewModel.cs
index c5b9f66207..4fb34e6b97 100644
--- a/Source/NETworkManager/ViewModels/TracerouteViewModel.cs
+++ b/Source/NETworkManager/ViewModels/TracerouteViewModel.cs
@@ -4,9 +4,9 @@
using NETworkManager.Controls;
using NETworkManager.Localization.Resources;
using NETworkManager.Models;
-using NETworkManager.Models.EventSystem;
using NETworkManager.Models.Export;
using NETworkManager.Models.Network;
+using NETworkManager.Models.RDAP;
using NETworkManager.Settings;
using NETworkManager.Utilities;
using NETworkManager.Views;
@@ -307,7 +307,7 @@ private void RedirectDataToApplicationAction(object name)
? SelectedResult.Hostname
: SelectedResult.IPAddress.ToString();
- EventSystem.RedirectToApplication(applicationName, host);
+ NavigationManager.Open(new ApplicationRequest(applicationName) { Target = host });
}
///
@@ -317,19 +317,29 @@ private void RedirectDataToApplicationAction(object name)
private void PerformDNSLookupAction(object data)
{
- EventSystem.RedirectToApplication(ApplicationName.DNSLookup, data.ToString());
+ NavigationManager.Open(new DNSLookupRequest { Target = data.ToString() });
}
///
- /// Gets the command to query the registration data of the selected hop's IP address, hostname (its registered
- /// domain) or AS number via RDAP.
+ /// Gets the command to query the registration data of the selected hop's IP address or hostname (its registered
+ /// domain) via RDAP.
///
public ICommand PerformRDAPQueryCommand => new RelayCommand(PerformRDAPQueryAction);
private void PerformRDAPQueryAction(object data)
+ {
+ NavigationManager.Open(new RDAPRequest { Target = data.ToString() });
+ }
+
+ ///
+ /// Gets the command to query the registration data of the selected hop's AS number via RDAP.
+ ///
+ public ICommand PerformRDAPQueryASNCommand => new RelayCommand(PerformRDAPQueryASNAction);
+
+ private void PerformRDAPQueryASNAction(object data)
{
// The AS of the IP geolocation contains the name as well (e.g. "AS15169 Google LLC").
- EventSystem.RedirectToApplication(ApplicationName.RDAP, data.ToString().Split(' ')[0]);
+ NavigationManager.Open(new RDAPRequest { Target = data.ToString().Split(' ')[0], QueryType = RDAPQueryType.ASN });
}
///
@@ -423,7 +433,8 @@ await DNSClientHelper.ResolveAorAaaaAsync(Host,
traceroute.TraceAsync(ipAddress, _cancellationTokenSource.Token);
// Add the host to history
- AddHostToHistory(Host);
+ ListHelper.Modify(SettingsManager.Current.Traceroute_HostHistory, Host,
+ SettingsManager.Current.General_HistoryListEntries);
}
catch (Exception ex) // This will catch any exception
{
@@ -480,20 +491,6 @@ await DialogHelper.ShowMessageAsync(window, Strings.Error,
return window.ShowChildWindowAsync(childWindow);
}
- private void AddHostToHistory(string host)
- {
- // Create the new list
- var list = ListHelper.Modify(SettingsManager.Current.Traceroute_HostHistory.ToList(), host,
- SettingsManager.Current.General_HistoryListEntries);
-
- // Clear the old items
- SettingsManager.Current.Traceroute_HostHistory.Clear();
- OnPropertyChanged(nameof(Host)); // Raise property changed again, after the collection has been cleared
-
- // Fill with the new items
- list.ForEach(x => SettingsManager.Current.Traceroute_HostHistory.Add(x));
- }
-
public void OnClose()
{
// Prevent multiple calls
diff --git a/Source/NETworkManager/ViewModels/WakeOnLANViewModel.cs b/Source/NETworkManager/ViewModels/WakeOnLANViewModel.cs
index 335d64d2f8..81a8ed8e66 100644
--- a/Source/NETworkManager/ViewModels/WakeOnLANViewModel.cs
+++ b/Source/NETworkManager/ViewModels/WakeOnLANViewModel.cs
@@ -158,10 +158,6 @@ public WakeOnLANViewModel()
protected override ApplicationName ApplicationName => ApplicationName.WakeOnLAN;
- protected override bool IsProfileEnabled(ProfileInfo profile) => profile.WakeOnLAN_Enabled;
-
- protected override string GetSearchableField(ProfileInfo profile) => profile.WakeOnLAN_MACAddress;
-
#endregion
#region ICommands & Actions
@@ -187,8 +183,10 @@ private void WakeUpAction()
Port = SettingsManager.Current.WakeOnLAN_Port
};
- AddMACAddressToHistory(MACAddress);
- AddBroadcastToHistory(Broadcast);
+ ListHelper.Modify(SettingsManager.Current.WakeOnLan_MACAddressHistory, MACAddress,
+ SettingsManager.Current.General_HistoryListEntries);
+ ListHelper.Modify(SettingsManager.Current.WakeOnLan_BroadcastHistory, Broadcast,
+ SettingsManager.Current.General_HistoryListEntries);
_ = WakeUp(info);
}
@@ -200,13 +198,39 @@ private void WakeUpAction()
private void WakeUpProfileAction()
{
- _ = WakeUp(NETworkManager.Profiles.Application.WakeOnLAN.CreateInfo(SelectedProfile));
+ WakeUp(SelectedProfile);
}
#endregion
#region Methods
+ ///
+ /// Sends the magic packet to the computer of the profile.
+ ///
+ /// Profile to wake up.
+ public void WakeUp(ProfileInfo profile)
+ {
+ _ = WakeUp(NETworkManager.Profiles.Application.WakeOnLAN.CreateInfo(profile));
+ }
+
+ ///
+ /// Sets the MAC address (and broadcast address) and sends the magic packet, if the broadcast address is valid
+ /// (e.g. from a previous wake up). Otherwise, the values are only filled in.
+ ///
+ /// MAC address of the computer.
+ /// Broadcast address. If null, the current broadcast address is used.
+ public void WakeUp(string macAddress, string broadcast)
+ {
+ MACAddress = macAddress;
+
+ if (broadcast != null)
+ Broadcast = broadcast;
+
+ if (Broadcast != null && RegexHelper.IPv4AddressRegex().IsMatch(Broadcast))
+ WakeUpAction();
+ }
+
private async Task WakeUp(WakeOnLANInfo info)
{
IsStatusMessageDisplayed = false;
@@ -231,33 +255,5 @@ private async Task WakeUp(WakeOnLANInfo info)
IsRunning = false;
}
- private void AddMACAddressToHistory(string macAddress)
- {
- // Create the new list
- var list = ListHelper.Modify(SettingsManager.Current.WakeOnLan_MACAddressHistory.ToList(), macAddress,
- SettingsManager.Current.General_HistoryListEntries);
-
- // Clear the old items
- SettingsManager.Current.WakeOnLan_MACAddressHistory.Clear();
- OnPropertyChanged(nameof(MACAddress)); // Raise property changed again, after the collection has been cleared
-
- // Fill with the new items
- list.ForEach(x => SettingsManager.Current.WakeOnLan_MACAddressHistory.Add(x));
- }
-
- private void AddBroadcastToHistory(string broadcast)
- {
- // Create the new list
- var list = ListHelper.Modify(SettingsManager.Current.WakeOnLan_BroadcastHistory.ToList(), broadcast,
- SettingsManager.Current.General_HistoryListEntries);
-
- // Clear the old items
- SettingsManager.Current.WakeOnLan_BroadcastHistory.Clear();
- OnPropertyChanged(nameof(Broadcast)); // Raise property changed again, after the collection has been cleared
-
- // Fill with the new items
- list.ForEach(x => SettingsManager.Current.WakeOnLan_BroadcastHistory.Add(x));
- }
-
#endregion
}
diff --git a/Source/NETworkManager/ViewModels/WebConsoleHostViewModel.cs b/Source/NETworkManager/ViewModels/WebConsoleHostViewModel.cs
index 951174d9eb..e359419d6a 100644
--- a/Source/NETworkManager/ViewModels/WebConsoleHostViewModel.cs
+++ b/Source/NETworkManager/ViewModels/WebConsoleHostViewModel.cs
@@ -4,7 +4,6 @@
using NETworkManager.Controls;
using NETworkManager.Localization.Resources;
using NETworkManager.Models;
-using NETworkManager.Models.EventSystem;
using NETworkManager.Models.WebConsole;
using NETworkManager.Profiles;
using NETworkManager.Profiles.Application;
@@ -100,10 +99,6 @@ public WebConsoleHostViewModel()
protected override ApplicationName ApplicationName => ApplicationName.WebConsole;
- protected override bool IsProfileEnabled(ProfileInfo profile) => profile.WebConsole_Enabled;
-
- protected override string GetSearchableField(ProfileInfo profile) => profile.WebConsole_Url;
-
public override void OnProfileManagerDialogOpen()
{
ConfigurationManager.OnDialogOpen();
@@ -150,14 +145,14 @@ private bool ConnectProfile_CanExecute(object obj)
private void ConnectProfileAction()
{
- ConnectProfile();
+ ConnectProfile(SelectedProfile);
}
public ICommand OpenSettingsCommand => new RelayCommand(_ => OpenSettingsAction());
private static void OpenSettingsAction()
{
- EventSystem.RedirectToSettings();
+ NavigationManager.OpenSettings();
}
public ICommand OpenWebsiteCommand => new RelayCommand(OpenWebsiteAction);
@@ -171,7 +166,7 @@ private static void OpenWebsiteAction(object url)
#region Methods
- private Task Connect()
+ private Task Connect(string url = null)
{
var childWindow = new WebConsoleConnectChildWindow();
@@ -191,7 +186,8 @@ private Task Connect()
// Add to history
// Note: The history can only be updated after the values have been read.
// Otherwise, in some cases, incorrect values are taken over.
- AddUrlToHistory(instance.Url);
+ ListHelper.Modify(SettingsManager.Current.WebConsole_UrlHistory, instance.Url,
+ SettingsManager.Current.General_HistoryListEntries);
Connect(info);
}, _ =>
@@ -200,7 +196,10 @@ private Task Connect()
ConfigurationManager.Current.IsChildWindowOpen = false;
ConfigurationManager.OnDialogClose();
- });
+ })
+ {
+ Url = url
+ };
childWindow.Title = Strings.Connect;
@@ -213,10 +212,22 @@ private Task Connect()
return Application.Current.MainWindow.ShowChildWindowAsync(childWindow);
}
- private void ConnectProfile()
+ ///
+ /// Connects to the profile.
+ ///
+ /// Profile to connect to.
+ public void ConnectProfile(ProfileInfo profileInfo)
+ {
+ Connect(WebConsole.CreateSessionInfo(profileInfo), profileInfo.Name);
+ }
+
+ ///
+ /// Opens the connect dialog with the URL.
+ ///
+ /// URL to connect to.
+ public void AddTab(string url)
{
- Connect(WebConsole.CreateSessionInfo(SelectedProfile),
- SelectedProfile.Name);
+ _ = Connect(url);
}
private void Connect(WebConsoleSessionInfo sessionInfo, string header = null)
@@ -229,15 +240,5 @@ private void Connect(WebConsoleSessionInfo sessionInfo, string header = null)
}
// Modify history list
- private static void AddUrlToHistory(string url)
- {
- if (string.IsNullOrEmpty(url))
- return;
-
- SettingsManager.Current.WebConsole_UrlHistory = new ObservableCollection(
- ListHelper.Modify(SettingsManager.Current.WebConsole_UrlHistory.ToList(), url,
- SettingsManager.Current.General_HistoryListEntries));
- }
-
#endregion
}
diff --git a/Source/NETworkManager/ViewModels/WhoisHostViewModel.cs b/Source/NETworkManager/ViewModels/WhoisHostViewModel.cs
index 846daada81..b4954a91d1 100644
--- a/Source/NETworkManager/ViewModels/WhoisHostViewModel.cs
+++ b/Source/NETworkManager/ViewModels/WhoisHostViewModel.cs
@@ -70,10 +70,6 @@ public WhoisHostViewModel()
protected override ApplicationName ApplicationName => ApplicationName.Whois;
- protected override bool IsProfileEnabled(ProfileInfo profile) => profile.Whois_Enabled;
-
- protected override string GetSearchableField(ProfileInfo profile) => profile.Whois_Domain;
-
#endregion
#region ICommand & Actions
@@ -94,7 +90,7 @@ private bool QueryProfile_CanExecute(object obj)
private void QueryProfileAction()
{
- AddTab(SelectedProfile.Whois_Domain);
+ AddTab(SelectedProfile);
}
public ItemActionCallback CloseItemCommand => CloseItemAction;
@@ -118,5 +114,10 @@ public void AddTab(string domain = null)
SelectedTabIndex = TabItems.Count - 1;
}
+ public void AddTab(ProfileInfo profile)
+ {
+ AddTab(profile.Whois_Domain);
+ }
+
#endregion
}
diff --git a/Source/NETworkManager/ViewModels/WhoisViewModel.cs b/Source/NETworkManager/ViewModels/WhoisViewModel.cs
index c272c874ca..9a6aeed3c5 100644
--- a/Source/NETworkManager/ViewModels/WhoisViewModel.cs
+++ b/Source/NETworkManager/ViewModels/WhoisViewModel.cs
@@ -192,7 +192,8 @@ private async Task Query()
Result = await Whois.QueryAsync(Domain, whoisServer);
IsResultVisible = true;
- AddDomainToHistory(Domain);
+ ListHelper.Modify(SettingsManager.Current.Whois_DomainHistory, Domain,
+ SettingsManager.Current.General_HistoryListEntries);
}
}
catch (Exception ex)
@@ -215,20 +216,6 @@ public void OnClose()
ConfigurationManager.Current.WhoisTabCount--;
}
- private void AddDomainToHistory(string domain)
- {
- // Create the new list
- var list = ListHelper.Modify(SettingsManager.Current.Whois_DomainHistory.ToList(), domain,
- SettingsManager.Current.General_HistoryListEntries);
-
- // Clear the old items
- SettingsManager.Current.Whois_DomainHistory.Clear();
- OnPropertyChanged(nameof(Domain)); // Raise property changed again, after the collection has been cleared
-
- // Fill with the new items
- list.ForEach(x => SettingsManager.Current.Whois_DomainHistory.Add(x));
- }
-
private Task Export()
{
var window = Application.Current.Windows.OfType().FirstOrDefault(x => x.IsActive);
diff --git a/Source/NETworkManager/Views/DNSLookupHostView.xaml.cs b/Source/NETworkManager/Views/DNSLookupHostView.xaml.cs
index fc6e032285..1b1f604aff 100644
--- a/Source/NETworkManager/Views/DNSLookupHostView.xaml.cs
+++ b/Source/NETworkManager/Views/DNSLookupHostView.xaml.cs
@@ -2,7 +2,7 @@
namespace NETworkManager.Views;
-public partial class DNSLookupHostView
+public partial class DNSLookupHostView : IApplicationRequestHandler
{
private readonly DNSLookupHostViewModel _viewModel = new();
@@ -12,9 +12,17 @@ public DNSLookupHostView()
DataContext = _viewModel;
}
- public void AddTab(string host)
+ public void Open(ApplicationRequest request)
{
- _viewModel.AddTab(host);
+ if (request.Profile != null)
+ {
+ _viewModel.AddTab(request.Profile);
+ return;
+ }
+
+ var dnsLookupRequest = request as DNSLookupRequest;
+
+ _viewModel.AddTab(request.Target, dnsLookupRequest?.DNSServer, dnsLookupRequest?.QueryType);
}
public void OnViewHide()
diff --git a/Source/NETworkManager/Views/DNSLookupView.xaml.cs b/Source/NETworkManager/Views/DNSLookupView.xaml.cs
index b00f030485..8edfa206d4 100644
--- a/Source/NETworkManager/Views/DNSLookupView.xaml.cs
+++ b/Source/NETworkManager/Views/DNSLookupView.xaml.cs
@@ -1,6 +1,7 @@
using System;
using System.Windows;
using System.Windows.Controls;
+using DnsClient;
using NETworkManager.Controls;
using NETworkManager.ViewModels;
@@ -10,11 +11,11 @@ public partial class DNSLookupView : IDragablzTabItem
{
private readonly DNSLookupViewModel _viewModel;
- public DNSLookupView(Guid tabId, string host = null)
+ public DNSLookupView(Guid tabId, string host = null, string dnsServer = null, QueryType? queryType = null)
{
InitializeComponent();
- _viewModel = new DNSLookupViewModel(tabId, host);
+ _viewModel = new DNSLookupViewModel(tabId, host, dnsServer, queryType);
DataContext = _viewModel;
diff --git a/Source/NETworkManager/Views/IPGeolocationHostView.xaml.cs b/Source/NETworkManager/Views/IPGeolocationHostView.xaml.cs
index 4f8813acd3..43dc89ec95 100644
--- a/Source/NETworkManager/Views/IPGeolocationHostView.xaml.cs
+++ b/Source/NETworkManager/Views/IPGeolocationHostView.xaml.cs
@@ -2,7 +2,7 @@
namespace NETworkManager.Views;
-public partial class IPGeolocationHostView
+public partial class IPGeolocationHostView : IApplicationRequestHandler
{
private readonly IPGeolocationHostViewModel _viewModel = new();
@@ -12,6 +12,14 @@ public IPGeolocationHostView()
DataContext = _viewModel;
}
+ public void Open(ApplicationRequest request)
+ {
+ if (request.Profile != null)
+ _viewModel.AddTab(request.Profile);
+ else
+ _viewModel.AddTab(request.Target);
+ }
+
public void OnViewHide()
{
_viewModel.OnViewHide();
diff --git a/Source/NETworkManager/Views/IPScannerHostView.xaml.cs b/Source/NETworkManager/Views/IPScannerHostView.xaml.cs
index 25d1d3a18d..6593c40508 100644
--- a/Source/NETworkManager/Views/IPScannerHostView.xaml.cs
+++ b/Source/NETworkManager/Views/IPScannerHostView.xaml.cs
@@ -2,7 +2,7 @@
namespace NETworkManager.Views;
-public partial class IPScannerHostView
+public partial class IPScannerHostView : IApplicationRequestHandler
{
private readonly IPScannerHostViewModel _viewModel = new();
@@ -12,9 +12,12 @@ public IPScannerHostView()
DataContext = _viewModel;
}
- public void AddTab(string host)
+ public void Open(ApplicationRequest request)
{
- _viewModel.AddTab(host);
+ if (request.Profile != null)
+ _viewModel.AddTab(request.Profile);
+ else
+ _viewModel.AddTab(request.Target);
}
public void OnViewHide()
diff --git a/Source/NETworkManager/Views/PingMonitorHostView.xaml.cs b/Source/NETworkManager/Views/PingMonitorHostView.xaml.cs
index 42366d6a88..7ff2b61c9b 100644
--- a/Source/NETworkManager/Views/PingMonitorHostView.xaml.cs
+++ b/Source/NETworkManager/Views/PingMonitorHostView.xaml.cs
@@ -4,7 +4,7 @@
namespace NETworkManager.Views;
-public partial class PingMonitorHostView
+public partial class PingMonitorHostView : IApplicationRequestHandler
{
private readonly PingMonitorHostViewModel _viewModel = new();
@@ -14,10 +14,12 @@ public PingMonitorHostView()
DataContext = _viewModel;
}
- public void AddHost(string host)
+ public void Open(ApplicationRequest request)
{
- if (_viewModel.SetHost(host))
- _ = _viewModel.Start();
+ if (request.Profile != null)
+ _viewModel.AddHost(request.Profile);
+ else
+ _viewModel.AddHost(request.Target);
}
public void OnViewHide()
diff --git a/Source/NETworkManager/Views/PortScannerHostView.xaml.cs b/Source/NETworkManager/Views/PortScannerHostView.xaml.cs
index 30e612a289..99d078cc8c 100644
--- a/Source/NETworkManager/Views/PortScannerHostView.xaml.cs
+++ b/Source/NETworkManager/Views/PortScannerHostView.xaml.cs
@@ -2,7 +2,7 @@
namespace NETworkManager.Views;
-public partial class PortScannerHostView
+public partial class PortScannerHostView : IApplicationRequestHandler
{
private readonly PortScannerHostViewModel _viewModel = new();
@@ -12,9 +12,12 @@ public PortScannerHostView()
DataContext = _viewModel;
}
- public void AddTab(string host)
+ public void Open(ApplicationRequest request)
{
- _viewModel.AddTab(host);
+ if (request.Profile != null)
+ _viewModel.AddTab(request.Profile);
+ else
+ _viewModel.AddTab(request.Target, (request as PortScannerRequest)?.Ports);
}
public void OnViewHide()
diff --git a/Source/NETworkManager/Views/PowerShellHostView.xaml b/Source/NETworkManager/Views/PowerShellHostView.xaml
index 4b402065b6..2b2966419a 100644
--- a/Source/NETworkManager/Views/PowerShellHostView.xaml
+++ b/Source/NETworkManager/Views/PowerShellHostView.xaml
@@ -18,7 +18,6 @@
xmlns:wpfHelpers="clr-namespace:NETworkManager.Utilities.WPF;assembly=NETworkManager.Utilities.WPF"
xmlns:networkManager="clr-namespace:NETworkManager"
xmlns:hostControls="clr-namespace:NETworkManager.Views.Controls"
- Loaded="UserControl_Loaded"
mc:Ignorable="d" d:DataContext="{d:DesignInstance viewModels:PowerShellHostViewModel}">
diff --git a/Source/NETworkManager/Views/PowerShellHostView.xaml.cs b/Source/NETworkManager/Views/PowerShellHostView.xaml.cs
index 9c80c79016..bc3e33362e 100644
--- a/Source/NETworkManager/Views/PowerShellHostView.xaml.cs
+++ b/Source/NETworkManager/Views/PowerShellHostView.xaml.cs
@@ -1,35 +1,32 @@
+using NETworkManager.Utilities.WPF;
using NETworkManager.ViewModels;
-using System.Threading.Tasks;
-using System.Windows;
namespace NETworkManager.Views;
-public partial class PowerShellHostView
+public partial class PowerShellHostView : IApplicationRequestHandler
{
private readonly PowerShellHostViewModel _viewModel = new();
- private bool _loaded;
-
public PowerShellHostView()
{
InitializeComponent();
DataContext = _viewModel;
}
- private void UserControl_Loaded(object sender, RoutedEventArgs e)
- {
- _loaded = true;
- }
-
- public async void AddTab(string host)
+ public async void Open(ApplicationRequest request)
{
// Wait for the interface to load, before displaying the dialog to connect a new profile...
// MahApps will throw an exception...
- while (!_loaded)
- await Task.Delay(250);
+ if (!await this.WaitForLoadedAsync())
+ return;
+
+ if (!_viewModel.IsExecutableConfigured)
+ return;
- if (_viewModel.IsExecutableConfigured)
- _viewModel.AddTab(host);
+ if (request.Profile != null)
+ _viewModel.ConnectProfile(request.Profile);
+ else
+ _viewModel.AddTab(request.Target);
}
public void OnViewHide()
diff --git a/Source/NETworkManager/Views/PuTTYHostView.xaml b/Source/NETworkManager/Views/PuTTYHostView.xaml
index f158446330..ea813e7f0c 100644
--- a/Source/NETworkManager/Views/PuTTYHostView.xaml
+++ b/Source/NETworkManager/Views/PuTTYHostView.xaml
@@ -18,7 +18,6 @@
xmlns:wpfHelpers="clr-namespace:NETworkManager.Utilities.WPF;assembly=NETworkManager.Utilities.WPF"
xmlns:networkManager="clr-namespace:NETworkManager"
xmlns:hostControls="clr-namespace:NETworkManager.Views.Controls"
- Loaded="UserControl_Loaded"
mc:Ignorable="d" d:DataContext="{d:DesignInstance viewModels:PuTTYHostViewModel}">
diff --git a/Source/NETworkManager/Views/PuTTYHostView.xaml.cs b/Source/NETworkManager/Views/PuTTYHostView.xaml.cs
index 819d3310cb..b14aef3d1b 100644
--- a/Source/NETworkManager/Views/PuTTYHostView.xaml.cs
+++ b/Source/NETworkManager/Views/PuTTYHostView.xaml.cs
@@ -1,35 +1,32 @@
+using NETworkManager.Utilities.WPF;
using NETworkManager.ViewModels;
-using System.Threading.Tasks;
-using System.Windows;
namespace NETworkManager.Views;
-public partial class PuTTYHostView
+public partial class PuTTYHostView : IApplicationRequestHandler
{
private readonly PuTTYHostViewModel _viewModel = new();
- private bool _loaded;
-
public PuTTYHostView()
{
InitializeComponent();
DataContext = _viewModel;
}
- private void UserControl_Loaded(object sender, RoutedEventArgs e)
- {
- _loaded = true;
- }
-
- public async void AddTab(string host)
+ public async void Open(ApplicationRequest request)
{
// Wait for the interface to load, before displaying the dialog to connect a new profile...
// MahApps will throw an exception...
- while (!_loaded)
- await Task.Delay(250);
+ if (!await this.WaitForLoadedAsync())
+ return;
+
+ if (!_viewModel.IsExecutableConfigured)
+ return;
- if (_viewModel.IsExecutableConfigured)
- _viewModel.AddTab(host);
+ if (request.Profile != null)
+ _viewModel.ConnectProfile(request.Profile);
+ else
+ _viewModel.AddTab(request.Target);
}
public void OnViewHide()
diff --git a/Source/NETworkManager/Views/RDAPHostView.xaml.cs b/Source/NETworkManager/Views/RDAPHostView.xaml.cs
index 13405e9924..36118d9575 100644
--- a/Source/NETworkManager/Views/RDAPHostView.xaml.cs
+++ b/Source/NETworkManager/Views/RDAPHostView.xaml.cs
@@ -2,7 +2,7 @@
namespace NETworkManager.Views;
-public partial class RDAPHostView
+public partial class RDAPHostView : IApplicationRequestHandler
{
private readonly RDAPHostViewModel _viewModel = new();
@@ -12,9 +12,12 @@ public RDAPHostView()
DataContext = _viewModel;
}
- public void AddTab(string query)
+ public void Open(ApplicationRequest request)
{
- _viewModel.AddTab(query);
+ if (request.Profile != null)
+ _viewModel.AddTab(request.Profile);
+ else
+ _viewModel.AddTab(request.Target, (request as RDAPRequest)?.QueryType);
}
public void OnViewHide()
diff --git a/Source/NETworkManager/Views/RemoteDesktopHostView.xaml b/Source/NETworkManager/Views/RemoteDesktopHostView.xaml
index a47620e7b8..6ed47f1c38 100644
--- a/Source/NETworkManager/Views/RemoteDesktopHostView.xaml
+++ b/Source/NETworkManager/Views/RemoteDesktopHostView.xaml
@@ -17,7 +17,6 @@
xmlns:wpfHelpers="clr-namespace:NETworkManager.Utilities.WPF;assembly=NETworkManager.Utilities.WPF"
xmlns:networkManager="clr-namespace:NETworkManager"
xmlns:hostControls="clr-namespace:NETworkManager.Views.Controls"
- Loaded="UserControl_Loaded"
mc:Ignorable="d" d:DataContext="{d:DesignInstance viewModels:RemoteDesktopHostViewModel}">
diff --git a/Source/NETworkManager/Views/RemoteDesktopHostView.xaml.cs b/Source/NETworkManager/Views/RemoteDesktopHostView.xaml.cs
index d33633292b..92040196a3 100644
--- a/Source/NETworkManager/Views/RemoteDesktopHostView.xaml.cs
+++ b/Source/NETworkManager/Views/RemoteDesktopHostView.xaml.cs
@@ -1,34 +1,29 @@
+using NETworkManager.Utilities.WPF;
using NETworkManager.ViewModels;
-using System.Threading.Tasks;
-using System.Windows;
namespace NETworkManager.Views;
-public partial class RemoteDesktopHostView
+public partial class RemoteDesktopHostView : IApplicationRequestHandler
{
private readonly RemoteDesktopHostViewModel _viewModel = new();
- private bool _loaded;
-
public RemoteDesktopHostView()
{
InitializeComponent();
DataContext = _viewModel;
}
- private void UserControl_Loaded(object sender, RoutedEventArgs e)
- {
- _loaded = true;
- }
-
- public async void AddTab(string host)
+ public async void Open(ApplicationRequest request)
{
// Wait for the interface to load, before displaying the dialog to connect a new Profile...
// MahApps will throw an exception...
- while (!_loaded)
- await Task.Delay(250);
+ if (!await this.WaitForLoadedAsync())
+ return;
- _viewModel.AddTab(host);
+ if (request.Profile != null)
+ _viewModel.ConnectProfile(request.Profile);
+ else
+ _viewModel.AddTab(request.Target);
}
public void OnViewHide()
diff --git a/Source/NETworkManager/Views/RunCommandChildWindow.xaml b/Source/NETworkManager/Views/RunCommandChildWindow.xaml
new file mode 100644
index 0000000000..3b90ff6d5a
--- /dev/null
+++ b/Source/NETworkManager/Views/RunCommandChildWindow.xaml
@@ -0,0 +1,195 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/NETworkManager/Views/RunCommandChildWindow.xaml.cs b/Source/NETworkManager/Views/RunCommandChildWindow.xaml.cs
new file mode 100644
index 0000000000..f3ccd7cd0c
--- /dev/null
+++ b/Source/NETworkManager/Views/RunCommandChildWindow.xaml.cs
@@ -0,0 +1,74 @@
+using NETworkManager.ViewModels;
+using System;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Threading;
+
+namespace NETworkManager.Views;
+
+///
+/// Run command dialog. The input keeps the focus; Up/Down select a suggestion, Tab completes it, Enter runs it and
+/// Escape closes the dialog.
+///
+public partial class RunCommandChildWindow
+{
+ public RunCommandChildWindow()
+ {
+ InitializeComponent();
+ }
+
+ private RunCommandViewModel ViewModel => (RunCommandViewModel)DataContext;
+
+ private void ChildWindow_OnLoaded(object sender, RoutedEventArgs e)
+ {
+ Dispatcher.BeginInvoke(DispatcherPriority.ContextIdle, new Action(delegate { TextBoxInput.Focus(); }));
+ }
+
+ private void TextBoxInput_OnPreviewKeyDown(object sender, KeyEventArgs e)
+ {
+ switch (e.Key)
+ {
+ case Key.Down:
+ ViewModel.SelectNext();
+ break;
+ case Key.Up:
+ ViewModel.SelectPrevious();
+ break;
+ case Key.Tab:
+ ViewModel.Complete();
+ TextBoxInput.CaretIndex = TextBoxInput.Text.Length;
+ break;
+ case Key.Enter:
+ ViewModel.Run();
+ break;
+ case Key.Escape:
+ ViewModel.CancelCommand.Execute(null);
+ break;
+ default:
+ return;
+ }
+
+ e.Handled = true;
+ }
+
+ private void ListBoxSuggestions_OnPreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
+ {
+ if (ItemsControl.ContainerFromElement(ListBoxSuggestions, (DependencyObject)e.OriginalSource) is not
+ ListBoxItem item)
+ return;
+
+ ViewModel.SelectedSuggestion = (RunCommandSuggestion)item.DataContext;
+ ViewModel.Run();
+
+ // The dialog stays open if the input is not valid (otherwise the opened application or dialog gets the focus).
+ if (IsOpen)
+ TextBoxInput.Focus();
+ }
+
+ private void ListBoxSuggestions_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (ListBoxSuggestions.SelectedItem != null)
+ ListBoxSuggestions.ScrollIntoView(ListBoxSuggestions.SelectedItem);
+ }
+}
diff --git a/Source/NETworkManager/Views/SNMPHostView.xaml.cs b/Source/NETworkManager/Views/SNMPHostView.xaml.cs
index fb7a33a8b7..2755b8e09e 100644
--- a/Source/NETworkManager/Views/SNMPHostView.xaml.cs
+++ b/Source/NETworkManager/Views/SNMPHostView.xaml.cs
@@ -2,7 +2,7 @@
namespace NETworkManager.Views;
-public partial class SNMPHostView
+public partial class SNMPHostView : IApplicationRequestHandler
{
private readonly SNMPHostViewModel _viewModel = new();
@@ -12,9 +12,12 @@ public SNMPHostView()
DataContext = _viewModel;
}
- public void AddTab(string host)
+ public void Open(ApplicationRequest request)
{
- _viewModel.AddTab(host);
+ if (request.Profile != null)
+ _viewModel.AddTab(request.Profile);
+ else
+ _viewModel.AddTab(request.Target);
}
public void OnViewHide()
diff --git a/Source/NETworkManager/Views/TigerVNCHostView.xaml b/Source/NETworkManager/Views/TigerVNCHostView.xaml
index 228da7c123..a50e0a27d7 100644
--- a/Source/NETworkManager/Views/TigerVNCHostView.xaml
+++ b/Source/NETworkManager/Views/TigerVNCHostView.xaml
@@ -17,7 +17,6 @@
xmlns:wpfHelpers="clr-namespace:NETworkManager.Utilities.WPF;assembly=NETworkManager.Utilities.WPF"
xmlns:networkManager="clr-namespace:NETworkManager"
xmlns:hostControls="clr-namespace:NETworkManager.Views.Controls"
- Loaded="UserControl_Loaded"
mc:Ignorable="d" d:DataContext="{d:DesignInstance viewModels:TigerVNCHostViewModel}">
diff --git a/Source/NETworkManager/Views/TigerVNCHostView.xaml.cs b/Source/NETworkManager/Views/TigerVNCHostView.xaml.cs
index d556299387..4164384504 100644
--- a/Source/NETworkManager/Views/TigerVNCHostView.xaml.cs
+++ b/Source/NETworkManager/Views/TigerVNCHostView.xaml.cs
@@ -1,35 +1,32 @@
+using NETworkManager.Utilities.WPF;
using NETworkManager.ViewModels;
-using System.Threading.Tasks;
-using System.Windows;
namespace NETworkManager.Views;
-public partial class TigerVNCHostView
+public partial class TigerVNCHostView : IApplicationRequestHandler
{
private readonly TigerVNCHostViewModel _viewModel = new();
- private bool _loaded;
-
public TigerVNCHostView()
{
InitializeComponent();
DataContext = _viewModel;
}
- private void UserControl_Loaded(object sender, RoutedEventArgs e)
- {
- _loaded = true;
- }
-
- public async void AddTab(string host)
+ public async void Open(ApplicationRequest request)
{
// Wait for the interface to load, before displaying the dialog to connect a new profile...
// MahApps will throw an exception...
- while (!_loaded)
- await Task.Delay(250);
+ if (!await this.WaitForLoadedAsync())
+ return;
+
+ if (!_viewModel.IsConfigured)
+ return;
- if (_viewModel.IsConfigured)
- _viewModel.AddTab(host);
+ if (request.Profile != null)
+ _viewModel.ConnectProfile(request.Profile);
+ else
+ _viewModel.AddTab(request.Target);
}
public void OnViewHide()
diff --git a/Source/NETworkManager/Views/TracerouteHostView.xaml.cs b/Source/NETworkManager/Views/TracerouteHostView.xaml.cs
index 2805de869b..84c6255eee 100644
--- a/Source/NETworkManager/Views/TracerouteHostView.xaml.cs
+++ b/Source/NETworkManager/Views/TracerouteHostView.xaml.cs
@@ -1,9 +1,8 @@
-using NETworkManager.Profiles;
using NETworkManager.ViewModels;
namespace NETworkManager.Views;
-public partial class TracerouteHostView
+public partial class TracerouteHostView : IApplicationRequestHandler
{
private readonly TracerouteHostViewModel _viewModel = new();
@@ -13,14 +12,12 @@ public TracerouteHostView()
DataContext = _viewModel;
}
- public void AddTab(string host)
+ public void Open(ApplicationRequest request)
{
- _viewModel.AddTab(host);
- }
-
- public void AddTab(ProfileInfo profile)
- {
- _viewModel.AddTab(profile);
+ if (request.Profile != null)
+ _viewModel.AddTab(request.Profile);
+ else
+ _viewModel.AddTab(request.Target);
}
public void OnViewHide()
diff --git a/Source/NETworkManager/Views/TracerouteView.xaml b/Source/NETworkManager/Views/TracerouteView.xaml
index f1cd088f0c..35b5eb05ad 100644
--- a/Source/NETworkManager/Views/TracerouteView.xaml
+++ b/Source/NETworkManager/Views/TracerouteView.xaml
@@ -285,7 +285,7 @@