Skip to content

Commit efdf21b

Browse files
BornToBeRootclaude
andauthored
Feature: Improve run command (Ctrl+Shift+P) (#3644)
* Refactor: NavigationManager with typed application requests Replaces the EventSystem (Models), which passed a single string, with NavigationManager in the main project: - ApplicationRequest (target or profile) with derived requests for options: DNSLookupRequest (DNS server, query type), PortScannerRequest (ports), RDAPRequest (query type). - Host views implement IApplicationRequestHandler; MainWindow passes the request to the shown view instead of a switch per application. - Profile actions of the tools are public (e.g. ConnectProfile(profile)) so a request can run a profile. - Web Console accepts a URL (connect dialog), like the other remote tools. - All callers migrated (IP Scanner, Traceroute, Network Interface, RDAP, open settings). Traceroute > RDAP > AS number queries with type ASN. - The application search is cleared before a request, so a filtered application list doesn't make the request fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Feature: Run command parser with unit tests - RunCommandParser (Models): "command target [argument] [type]" or "command @Profile". Whitespace around ";" is removed, so lists can be typed with spaces; a type keyword and the second argument (e.g. ports, DNS server) can be in any order; the rest after "@" is the profile name (names can contain spaces). - First test project of the solution (NETworkManager.Tests, xUnit v3) with tests for the parser. dotnet test uses Microsoft.Testing.Platform (global.json). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Feature: Run command dialog with arguments, profiles and suggestions - The run command (Ctrl+Shift+P) opens a dialog instead of the flyout: type e.g. "ping 10.0.0.1", "dns borntoberoot.net 1.1.1.1 aaaa" or "rdp @server01" and press Enter. Esc closes the dialog. - Suggestions: recent commands, applications (with aliases like ping, dns, rdp, ssh), profiles of the application (@name) and type values. Up/Down select, Tab completes, Enter runs the selection (the input as typed is preselected). - The target is validated with the validators of the application's input fields before the application is opened. - Only visible applications are offered. - Run command history (setting RunCommand_History). - Removed the flyout and the unused RunCommandComboBox. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Docs: Run command page and changelog Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix: Scroll to the selected application in the application list The application list only scrolled to the selected application on startup and after clearing the search. An application opened from somewhere else (dashboard, another application, run command) could stay out of view. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Refactor: Update input histories in place ListHelper.Modify works on the ObservableCollection itself: an existing entry is moved to the top, a new one inserted, and the oldest ones are removed. Replaces the pattern copy/clear/refill (with a workaround to restore the input text after the clear) and replacing the collection in 34 places in 22 view models. Unit tests for the helper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Refactor: Inline the AddXToHistory methods The 34 AddXToHistory methods only called ListHelper.Modify and were each called once. ListHelper.Modify now ignores default values (null, 0) and empty strings, which the remote tools checked in their methods, so the call sites call it directly. PuTTY keeps its check for port and baud, which are converted to strings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Refactor: Profile target and enabled check in ProfileInfo ProfileInfo.IsEnabled(application) and GetTarget(application) (the tool's host, IP range, domain, URL, ...) replace the IsProfileEnabled and GetSearchableField overrides of 16 view models. The profile panel and the run command use the same mappings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Refactor: Wait for the view to be loaded without polling WaitForLoadedAsync (Utilities.WPF) completes on the Loaded event (or immediately if the view is already loaded) and replaces the "while (!_loaded) await Task.Delay(250)" loops of the remote tools. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix: Run command review findings - Suggestions: exact command/alias first (e.g. "lookup" no longer opens DNS Lookup), exact profile name first in "@" mode, recent commands of a command before its profiles, the tool's target of a profile (e.g. RDP host) instead of the general host. - DNS servers: configured servers as suggestions, matched by name case-insensitively; values with spaces can be quoted ("Google Public DNS"). - DNS Lookup uses PTR for IP addresses from any request (run command, IP Scanner, Traceroute, profiles); shortened IPv4 forms like "10" are host names. - A failed suggestion (e.g. a recent command with an ambiguous profile name) is put into the input to correct it. - Syntax hints use resource strings; history entries are parsed once. - Docs: quotes, DNS servers, duplicate profile names, "@" only at the start of the target. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix: Run command second review findings, IP Geolocation and Wake on LAN - Enter runs what was typed: no preselected suggestion for an empty input or an unknown command with arguments (e.g. "pin 10.0.0.1"); completing a command keeps the arguments. - "@": a profile is only preselected if exactly one matches, so duplicate names show the error and the profiles to select. - Parser: a list that is being typed ("10.0.0.1; ") stays the target, ";" in quotes is kept, quotes around "@name" are removed. - Web Console requests require the WebView2 runtime. - The run command "settings" keeps the open settings page. - History and profiles are loaded once per dialog. - New commands: ipgeolocation (geo) and wakeonlan (wol <MAC> [broadcast]). - Application list: removed a redundant scroll on startup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix: Run command third review findings - Run command "settings" with a filtered application list no longer crashes (the search is cleared first). - The dialog has a title bar with a close button. - The DNS server argument is validated (configured name or IP address / host with a domain); a mistyped type like "aaa" is reported. - A request is only passed to the application if it is shown. - After running a suggestion with the mouse, the focus isn't taken back by the closing dialog. - History: entries of hidden applications are not shown; commands are stored case-insensitively (ListHelper.Modify with a comparer). - Text after a quoted profile name is an unknown argument. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix: Run command final review findings - The suggestion list is limited to 340 px, so it fits into the dialog (max. 500 px) and the selected entry is always visible. - WaitForLoadedAsync stops waiting after 5 seconds, so a request to a view that wasn't shown doesn't run later when the view is opened. - Completing a command uses the shortest matching name ("pin" → "ping"). - Profile names are compared like they are parsed (trimmed, without quotes), so recent commands with such names work. - Empty DNS server list entries ("1.1.1.1; ") are ignored. - Commands without arguments use the last word for the suggestions. - Command names are computed once per dialog; one close function for the run command dialog. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Docs: Add PR #3644 to changelog Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 2309fc2 commit efdf21b

90 files changed

Lines changed: 3016 additions & 1557 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎Source/NETworkManager.Localization/Resources/Strings.Designer.cs‎

Lines changed: 90 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Source/NETworkManager.Localization/Resources/Strings.resx‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4857,4 +4857,34 @@ Cloudflare may log your IP address and network information. See Cloudflare's pri
48574857
<data name="Resource_PublicSuffixList_Description" xml:space="preserve">
48584858
<value>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.</value>
48594859
</data>
4860+
<data name="ProfileXNotFound" xml:space="preserve">
4861+
<value>Profile "{0}" not found!</value>
4862+
</data>
4863+
<data name="SeveralProfilesAreNamedXSelectOne" xml:space="preserve">
4864+
<value>Several profiles are named "{0}", select one!</value>
4865+
</data>
4866+
<data name="EnterACommand" xml:space="preserve">
4867+
<value>Enter a command!</value>
4868+
</data>
4869+
<data name="UnknownCommandX" xml:space="preserve">
4870+
<value>Unknown command "{0}"!</value>
4871+
</data>
4872+
<data name="CommandXDoesNotAcceptArguments" xml:space="preserve">
4873+
<value>The command "{0}" does not accept arguments!</value>
4874+
</data>
4875+
<data name="CommandXDoesNotSupportProfiles" xml:space="preserve">
4876+
<value>The command "{0}" does not support profiles!</value>
4877+
</data>
4878+
<data name="EnterTheNameOfAProfileAfterAt" xml:space="preserve">
4879+
<value>Enter the name of a profile after "@"!</value>
4880+
</data>
4881+
<data name="UnknownArgumentX" xml:space="preserve">
4882+
<value>Unknown argument "{0}"!</value>
4883+
</data>
4884+
<data name="RunCommand" xml:space="preserve">
4885+
<value>Run command</value>
4886+
</data>
4887+
<data name="Recent" xml:space="preserve">
4888+
<value>Recent</value>
4889+
</data>
48604890
</root>

‎Source/NETworkManager.Models/EventSystem/EventSystem.cs‎

Lines changed: 0 additions & 22 deletions
This file was deleted.

‎Source/NETworkManager.Models/EventSystem/EventSystemRedirectArgs.cs‎

Lines changed: 0 additions & 15 deletions
This file was deleted.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace NETworkManager.Models.RunCommand;
2+
3+
/// <summary>
4+
/// Second argument of a run command after the target.
5+
/// </summary>
6+
public enum RunCommandArgument
7+
{
8+
None,
9+
Ports,
10+
DNSServer,
11+
Broadcast
12+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
5+
namespace NETworkManager.Models.RunCommand;
6+
7+
/// <summary>
8+
/// Command of the run command dialog (e.g. "dnslookup" with the alias "dns"), which opens an application or the
9+
/// settings.
10+
/// </summary>
11+
public sealed class RunCommandDefinition
12+
{
13+
/// <summary>
14+
/// Whether the command opens an application or the settings.
15+
/// </summary>
16+
public RunCommandType Type { get; init; }
17+
18+
/// <summary>
19+
/// Application to open (<see cref="ApplicationName.None" /> for the settings).
20+
/// </summary>
21+
public ApplicationName Application { get; init; }
22+
23+
/// <summary>
24+
/// Command (lowercase, e.g. "dnslookup").
25+
/// </summary>
26+
public string Command { get; init; }
27+
28+
/// <summary>
29+
/// Short forms of the command (lowercase, e.g. "dns", "nslookup").
30+
/// </summary>
31+
public IReadOnlyList<string> Aliases { get; init; } = [];
32+
33+
/// <summary>
34+
/// The command accepts a target (e.g. host, IP range, query).
35+
/// </summary>
36+
public bool SupportsTarget { get; init; }
37+
38+
/// <summary>
39+
/// The command accepts a profile ("@name") instead of a target.
40+
/// </summary>
41+
public bool SupportsProfile { get; init; }
42+
43+
/// <summary>
44+
/// Second argument after the target (e.g. ports or DNS server).
45+
/// </summary>
46+
public RunCommandArgument Argument { get; init; }
47+
48+
/// <summary>
49+
/// Type keywords that can follow the target (lowercase, e.g. "a", "aaaa" for the DNS lookup).
50+
/// </summary>
51+
public IReadOnlyList<string> Types { get; init; } = [];
52+
53+
/// <summary>
54+
/// Syntax of the arguments for the hint (e.g. "&lt;hosts&gt; [server] [type]"), empty if there are none.
55+
/// </summary>
56+
public string Syntax { get; init; } = string.Empty;
57+
58+
/// <summary>
59+
/// Checks if the text is the command or one of its aliases (case-insensitive).
60+
/// </summary>
61+
/// <param name="text">Command text.</param>
62+
/// <returns>True if the text matches.</returns>
63+
public bool Matches(string text)
64+
{
65+
return string.Equals(Command, text, StringComparison.OrdinalIgnoreCase) ||
66+
Aliases.Any(x => string.Equals(x, text, StringComparison.OrdinalIgnoreCase));
67+
}
68+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace NETworkManager.Models.RunCommand;
2+
3+
/// <summary>
4+
/// Errors of a run command input.
5+
/// </summary>
6+
public enum RunCommandParseError
7+
{
8+
None,
9+
Empty,
10+
UnknownCommand,
11+
NoArguments,
12+
NoProfiles,
13+
MissingProfileName,
14+
UnknownArgument
15+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
namespace NETworkManager.Models.RunCommand;
2+
3+
/// <summary>
4+
/// Result of <see cref="RunCommandParser.Parse" />.
5+
/// </summary>
6+
public sealed class RunCommandParseResult
7+
{
8+
/// <summary>
9+
/// Command that matches the first word, or null if there is none.
10+
/// </summary>
11+
public RunCommandDefinition Command { get; init; }
12+
13+
/// <summary>
14+
/// First word as typed.
15+
/// </summary>
16+
public string CommandText { get; init; } = string.Empty;
17+
18+
/// <summary>
19+
/// Target (e.g. "10.0.0.1;10.0.0.2"), or null if there is none.
20+
/// </summary>
21+
public string Target { get; init; }
22+
23+
/// <summary>
24+
/// Name of the profile ("@name"), or null if no profile is used. Empty if only "@" was typed.
25+
/// </summary>
26+
public string ProfileName { get; init; }
27+
28+
/// <summary>
29+
/// Second argument (e.g. ports or DNS server), or null if there is none.
30+
/// </summary>
31+
public string Argument { get; init; }
32+
33+
/// <summary>
34+
/// Type keyword in lowercase (e.g. "aaaa"), or null if there is none.
35+
/// </summary>
36+
public string Type { get; init; }
37+
38+
/// <summary>
39+
/// Part of the input that is being typed.
40+
/// </summary>
41+
public RunCommandPosition Position { get; init; }
42+
43+
/// <summary>
44+
/// Word that is being typed (empty after a space), used to filter the suggestions.
45+
/// </summary>
46+
public string CurrentWord { get; init; } = string.Empty;
47+
48+
/// <summary>
49+
/// Input without the word that is being typed (e.g. "dns borntoberoot.net " for "dns borntoberoot.net aa"),
50+
/// used to complete a suggestion.
51+
/// </summary>
52+
public string InputBeforeCurrentWord { get; init; } = string.Empty;
53+
54+
/// <summary>
55+
/// Error, if the input can't be run.
56+
/// </summary>
57+
public RunCommandParseError Error { get; init; }
58+
59+
/// <summary>
60+
/// Value the error refers to (e.g. the unknown command or argument).
61+
/// </summary>
62+
public string ErrorValue { get; init; }
63+
}

0 commit comments

Comments
 (0)