Quick connect: type user@host into Ctrl+K to open a terminal on a machine without saving it
ci / build and test (push) Successful in 5m48s
ci / api image (push) Successful in 47s
ci / deploy ssh.dodotech.cloud (push) Failing after 2s
ci / desktop nightly (push) Successful in 3m26s
ci / android head (push) Successful in 10m2s

The desktop had no way to reach a machine that was not in the keychain; the phone has had a manual
box on its terminal screen all along. The palette's query now reads as an address when it has an @
in it: a "not saved" row offers to dial it, Enter asks for the password in place, and a second Enter
connects. Escape on the password step goes back to the address.

It goes through the phone's own path, now split out as VaultViewModel.ConnectToEndpointAsync, so the
tab, the host-key question and the connection-log entry are the same on both heads and nothing is
written to the keychain. Password only, as on the phone: a key is the reason to save the host.

The typed row takes the highlight unless a saved host dials exactly that address, so typing a new
address cannot land on a saved machine whose address merely starts the same way.

Covered by eleven new QuickConnectTests; manual check 7.10a covers the real dial.
This commit is contained in:
jaap-jan committed 2026-10-14 17:02:26 +02:00
1 parent 902761d37b
commit 988248fa57
8 files changed
+840 -99

No files matched your search

@@ -39,11 +39,25 @@ internal sealed partial class QuickConnect : UserControl
// drag across the list — and the ListBox has moved its own selection by the time a tap completes,
// which is what makes the handler below a matter of reading the selection rather than hit-testing.
Results.Tapped += OnResultTapped;
AdHocRow.Tapped += OnAdHocRowTapped;
// The two steps swap in place, and whichever one has just appeared takes the caret: typing an address
// and pressing Enter should leave somebody typing a password, not looking for the box to click into.
// Watched on the steps themselves rather than on the view model, for the reason TakeKeyboard gives —
// the view model's change arrives ahead of the binding that reveals the step.
QueryStep.PropertyChanged += OnStepChanged;
PasswordStep.PropertyChanged += OnStepChanged;
}
/// <summary>The box, so the caret can be put in it the moment the palette opens.</summary>
internal TextBox QueryBox => Query;
/// <summary>The typed address's password box, which takes the caret when the palette asks for it.</summary>
internal TextBox PasswordBox => Password;
/// <summary>The row offering to dial what was typed, when it reads as an address.</summary>
internal Control AdHocTargetRow => AdHocRow;
private MainWindowViewModel? Shell => DataContext as MainWindowViewModel;
/// <summary>
@@ -63,6 +77,29 @@ internal sealed partial class QuickConnect : UserControl
return;
}
// The password step answers only Enter and Escape, and Escape there goes back to the address: a
// mistyped address is the likeliest reason to want out of it, and closing would throw that away too.
if (shell.IsAskingForAdHocPassword)
{
switch (e.Key)
{
case Key.Escape:
shell.BackToSearchQueryCommand.Execute(null);
e.Handled = true;
break;
case Key.Enter:
_ = shell.ConnectToAdHocTargetCommand.ExecuteAsync(null);
e.Handled = true;
break;
default:
break;
}
return;
}
switch (e.Key)
{
case Key.Escape:
@@ -76,12 +113,12 @@ internal sealed partial class QuickConnect : UserControl
break;
case Key.Down:
Move(shell, 1);
shell.MoveSearchSelection(1);
e.Handled = true;
break;
case Key.Up:
Move(shell, -1);
shell.MoveSearchSelection(-1);
e.Handled = true;
break;
@@ -129,23 +166,38 @@ internal sealed partial class QuickConnect : UserControl
NativeKeyboardFocus.ReturnTo(window);
}
Query.Focus();
if (PasswordStep.IsVisible)
{
Password.Focus();
}
else
{
Query.Focus();
}
}
/// <remarks>Clamped rather than wrapped: a list that jumps from the last row to the first loses people.</remarks>
private static void Move(MainWindowViewModel shell, int delta)
/// <summary>Hands the caret to whichever step has just been revealed.</summary>
/// <remarks>Posted, for the same reason the palette's own opening is: see <see cref="TakeKeyboard"/>.</remarks>
private void OnStepChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (shell.SearchResults.Count == 0)
if (e.Property == IsVisibleProperty && e.GetNewValue<bool>() && IsVisible)
{
Dispatcher.UIThread.Post(TakeKeyboard, DispatcherPriority.Loaded);
}
}
/// <summary>
/// Takes the typed address, for a click on its row: the pointer's version of Enter on it.
/// </summary>
private void OnAdHocRowTapped(object? sender, TappedEventArgs e)
{
if (Shell is not { IsSearching: true, HasAdHocTarget: true } shell)
{
return;
}
var current = shell.SelectedSearchResult is { } selected
? shell.SearchResults.IndexOf(selected)
: -1;
shell.SelectedSearchResult =
shell.SearchResults[Math.Clamp(current + delta, 0, shell.SearchResults.Count - 1)];
e.Handled = true;
shell.ChooseAdHocTargetCommand.Execute(null);
}
/// <inheritdoc />