diff --git a/src/DodoSSH.Client.App/Views/HostsScreen.axaml b/src/DodoSSH.Client.App/Views/HostsScreen.axaml
index 97bdbb0..88b8429 100644
--- a/src/DodoSSH.Client.App/Views/HostsScreen.axaml
+++ b/src/DodoSSH.Client.App/Views/HostsScreen.axaml
@@ -230,9 +230,13 @@
Ctrl+K is named on it because the palette is the other way to reach a host by typing, and somebody
who has found this box should know about the one that also connects on Enter.
+
+ One way, and passed back by the code-behind once the typing pauses: every change to the filter
+ rebuilds the board's sections and their cards, and a name typed at speed used to do that once a
+ letter. The view model still filters the moment it is told. See HostsScreen.OnFilterTyped.
-->
-
diff --git a/src/DodoSSH.Client.App/Views/HostsScreen.axaml.cs b/src/DodoSSH.Client.App/Views/HostsScreen.axaml.cs
index 794a00a..f873c71 100644
--- a/src/DodoSSH.Client.App/Views/HostsScreen.axaml.cs
+++ b/src/DodoSSH.Client.App/Views/HostsScreen.axaml.cs
@@ -106,6 +106,48 @@ internal sealed partial class HostsScreen : UserControl
Board.AddHandler(ContextRequestedEvent, OnContextRequested, RoutingStrategies.Tunnel);
WireTheBand();
+
+ HostFilter.TextChanged += OnFilterTyped;
+ }
+
+ /// How long the find box waits for the typing to pause before the board is rebuilt.
+ private static readonly TimeSpan FilterPause = TimeSpan.FromMilliseconds(150);
+
+ /// The wait itself, replaceable so a test can say when a pause is over rather than sleep through one.
+ internal Func Pause { get; set; } = static span => Task.Delay(span);
+
+ /// How many times the find box has changed, so a wait can tell it is no longer the latest.
+ private int typed;
+
+ /// Hands the find box's text to the vault once the typing pauses.
+ ///
+ ///
+ /// Every change to the filter rebuilds the board's sections and every card in them, and a name typed at
+ /// speed used to do that once a letter. Now it is done once, with the whole of what was typed.
+ ///
+ ///
+ /// A wait rather than a binding delay, and on the dispatcher's own queue: the box is bound one way, so a
+ /// filter the vault sets itself — cleared, or restored — still lands in the box at once, and the
+ /// change it makes to the box comes back here as the same text, which changes nothing.
+ ///
+ ///
+ private void OnFilterTyped(object? sender, TextChangedEventArgs e) =>
+ _ = PassTheFilterOnAsync(HostFilter.Text ?? string.Empty, ++typed);
+
+ private async Task PassTheFilterOnAsync(string text, int keystroke)
+ {
+ await Pause(FilterPause).ConfigureAwait(true);
+
+ // A later letter has its own wait, and it is the one that speaks.
+ if (keystroke != typed)
+ {
+ return;
+ }
+
+ if (Vault is { } vault && !string.Equals(vault.HostFilter, text, StringComparison.Ordinal))
+ {
+ vault.HostFilter = text;
+ }
}
/// The band's own wiring, and the two keys that go with a set.
diff --git a/tests/DodoSSH.Client.App.Layout.Tests/HostGridTests.cs b/tests/DodoSSH.Client.App.Layout.Tests/HostGridTests.cs
index c8a8424..0dcb961 100644
--- a/tests/DodoSSH.Client.App.Layout.Tests/HostGridTests.cs
+++ b/tests/DodoSSH.Client.App.Layout.Tests/HostGridTests.cs
@@ -520,6 +520,53 @@ public sealed class HostGridTests : IAsyncLifetime
});
}
+ ///
+ /// Every change to the filter rebuilds the board's sections and every card in them, and a name typed at
+ /// speed used to do that once a letter. The box now tells the vault once the typing pauses — and then
+ /// exactly once, with the whole of what was typed.
+ ///
+ [Fact]
+ public async Task TypingIntoTheFindBox_RebuildsTheBoardOnceThePauseComes_NotOnceALetter()
+ {
+ await OnTheBoardAsync((screen, _) =>
+ {
+ // Each letter's wait, ended by hand: the pause is real time in the application, and this says
+ // when it is over instead of sleeping through it.
+ var pauses = new List();
+
+ screen.Pause = _ =>
+ {
+ var pause = new TaskCompletionSource();
+ pauses.Add(pause);
+ return pause.Task;
+ };
+
+ var rebuilds = 0;
+ vault.HostSections.CollectionChanged += (_, _) => rebuilds++;
+
+ var box = screen.FindControl("HostFilter").ShouldNotBeNull();
+
+ box.Text = "p";
+ box.Text = "pr";
+ box.Text = "pro";
+ Dispatcher.UIThread.RunJobs();
+
+ vault.HostFilter.ShouldBeNullOrEmpty("nothing has reached the vault while the typing goes on");
+ rebuilds.ShouldBe(0);
+
+ foreach (var pause in pauses)
+ {
+ pause.SetResult();
+ }
+
+ Dispatcher.UIThread.RunJobs();
+
+ vault.HostFilter.ShouldBe("pro");
+ rebuilds.ShouldBe(1, "only the last letter's wait passes the text on");
+ vault.VisibleHosts.ShouldHaveSingleItem().Label.ShouldBe("prod-db");
+ });
+ }
+
// ---- Helpers ----
private static void RightClick(Visual card, Visual window)