From cdd5e7f6ec9dd735c478a7e0aec8ff75e4b222a2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 10:31:35 +0000 Subject: [PATCH] Run the menu's verbs on the path given with -p instead of asking again [patch] Menu is the default verb and inherits -p, so `FileDeduplicator -p dir` accepted the path, then every verb built by the menu prompted for it again. Each menu entry now hands the verb the menu's path before it runs, re-applied on every choice because BaseVerb.Run clears it. Fixes #177 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019BwRbgKQMY5pZraxHBjxqM --- FileDeduplicator.Test/MenuTests.cs | 25 ++++++++++++++++++++ FileDeduplicator/Verbs/Menu.cs | 38 ++++++++++++++++++++++++++---- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/FileDeduplicator.Test/MenuTests.cs b/FileDeduplicator.Test/MenuTests.cs index 0de466c..a650fd3 100644 --- a/FileDeduplicator.Test/MenuTests.cs +++ b/FileDeduplicator.Test/MenuTests.cs @@ -61,6 +61,31 @@ public void MenuRunsTheChosenVerbAndOffersTheMenuAgain() Assert.AreEqual(2, output.Split("0. Exit").Length - 1, "The menu was not offered again after the verb ran."); } + /// + /// FileDeduplicator -p dir with no verb lands on the menu, and the verb chosen from it + /// works on that directory rather than asking for a path again -- every time it is chosen. + /// + [TestMethod] + public void MenuHandsItsPathToTheChosenVerb() + { + // Arrange + using TempTree tree = new(); + _ = tree.Write("a.txt", "alpha"); + _ = tree.Write("aa.txt", "alpha"); + Type[] menuVerbs = [.. Program.Verbs.Where(t => t != typeof(Menu))]; + int scan = Array.IndexOf(menuVerbs, typeof(Scan)) + 1; + Assert.IsGreaterThan(0, scan, "Scan is not on the menu."); + + // Act -- no path on stdin: the only path is the one the menu was given + string output = ConsoleCapture.Run(new Menu { PathString = tree.Root.WeakString }, $"{scan}\n{scan}\n0\n", out int exitCode); + + // Assert + Assert.AreEqual(0, exitCode, output); + Assert.DoesNotContain("Enter the path", output, $"The menu dropped its -p path and asked again. Output was:\n{output}"); + Assert.AreEqual(2, output.Split($"Scanning: {tree.Root}").Length - 1, $"Scan did not work on the menu's path both times. Output was:\n{output}"); + Assert.AreEqual(2, output.Split("Found 1 group(s) of duplicate files").Length - 1, output); + } + [TestMethod] public void MenuRejectsAChoiceThatIsNotOnIt() { diff --git a/FileDeduplicator/Verbs/Menu.cs b/FileDeduplicator/Verbs/Menu.cs index 246af7d..39143c5 100644 --- a/FileDeduplicator/Verbs/Menu.cs +++ b/FileDeduplicator/Verbs/Menu.cs @@ -73,7 +73,7 @@ internal ControlRepeater BuildScrollMenu(out ExitCommand exit) private void RunNumberedMenu() { - BaseVerb[] verbs = [.. MenuVerbs.Select(CreateVerb)]; + ICommand[] verbs = [.. MenuVerbs.Select(CreateCommand)]; string[] texts = [.. MenuVerbs.Select(DescribeVerb)]; while (true) @@ -105,11 +105,23 @@ private void RunNumberedMenu() } } - private static BaseVerb CreateVerb(Type verbType) + /// + /// Creates the command a menu entry runs: the verb, handed the menu's own -p path each + /// time it is chosen. + /// + /// + /// The menu is the default verb, so FileDeduplicator -p dir lands here with the path set. + /// Dropping it would ask for the path again and invite a different one into a flow that can end + /// in deletion. It is re-applied on every run because clears the + /// verb's path when it finishes. + /// + /// The verb to run. + /// The command that runs it. + private PresetPathCommand CreateCommand(Type verbType) { BaseVerb? verb = Activator.CreateInstance(verbType) as BaseVerb; Debug.Assert(verb != null); - return verb; + return new(verb, () => PathString); } private static string DescribeVerb(Type verbType) @@ -119,13 +131,29 @@ private static string DescribeVerb(Type verbType) return string.IsNullOrEmpty(helpText) ? name : $"{name} - {helpText}"; } - private static LabelMenuItem CreateMenuItem(Type verbType) => new() + private LabelMenuItem CreateMenuItem(Type verbType) => new() { Text = DescribeVerb(verbType), - Command = CreateVerb(verbType), + Command = CreateCommand(verbType), IsEnabled = true, }; + /// + /// Runs a verb with the path the menu was given, or with none so that the verb asks for one. + /// + /// The verb to run. + /// Reads the menu's path at the moment the verb runs. + private sealed class PresetPathCommand(BaseVerb verb, Func path) : ICommand + { + public bool IsActive => verb.IsActive; + + public void Execute() + { + verb.PathString = path(); + verb.Execute(); + } + } + /// /// The menu item that leaves the menu, which otherwise could only be left with Ctrl+C. ///