diff --git a/docs/manual-checks.md b/docs/manual-checks.md index 6e3a2bd..19fa0ce 100644 --- a/docs/manual-checks.md +++ b/docs/manual-checks.md @@ -1404,6 +1404,23 @@ MACHINE box, which takes the same path — `VaultViewModel.ConnectToEndpointAsyn the two is in how the palette hands the password over, not in the connection. The headless half of this is `QuickConnectTests`; what it cannot do is dial. +### 7.10b Pressing Terminal with nothing open asks what to connect to + +Close every terminal tab, then press **Terminal** on the rail. + +**Pass:** the terminal's frame holds a prompt — `connect to:` with the caret already on it, so typing works +without a click. Type part of a saved host's name: the matching hosts appear under the line, Tab completes to +the first, and Enter connects to it — asking `user@host:port's password:` first only if that host has no key +or saved password. Type `user@host` for a machine that is not saved instead: Enter asks for its password, and +the second Enter opens a tab for it. Escape steps back from the password to the name. Afterwards HOSTS has +nothing new on it, and no credential was saved — even with "remember" ticked in a host's drawer earlier. + +**Failure means:** a prompt that needs a click before it takes typing is the terminal's WebView still holding +the native keyboard focus — see `NativeKeyboardFocus`, which `ConnectPrompt.TakeKeyboard` calls. Anything drawn +*under* a blank rectangle is the occlusion rule broken: the prompt may only show while no tab exists. A new +credential in KEYS after connecting is the prompt's attempt acting on the drawer's tick, which +`ShellFlowTests.ThePrompt_NeverSavesAPassword_EvenWithTheDrawersTickLeftOn` exists to stop. + ### 7.11 A host or a bucket made now can be picked on FILES now With the files screen's picker open on an empty or short list, go to HOSTS and add a host — or to KEYS and diff --git a/src/DodoSSH.Client.App/App.axaml b/src/DodoSSH.Client.App/App.axaml index 45f86fe..e36fcd4 100644 --- a/src/DodoSSH.Client.App/App.axaml +++ b/src/DodoSSH.Client.App/App.axaml @@ -873,6 +873,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/DodoSSH.Client.App/Views/ConnectPrompt.axaml.cs b/src/DodoSSH.Client.App/Views/ConnectPrompt.axaml.cs new file mode 100644 index 0000000..b7e861a --- /dev/null +++ b/src/DodoSSH.Client.App/Views/ConnectPrompt.axaml.cs @@ -0,0 +1,125 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Threading; +using DodoSSH.Client.Shell.ViewModels; + +namespace DodoSSH.Client.App.Views; + +/// The empty terminal's prompt: what to connect to, and its password when one is needed. +/// +/// The keys and the caret live here, as the palette's do in and for its reason: a +/// hosts in a bare window under the headless harness, and the window itself cannot +/// be shown there at all. +/// +internal sealed partial class ConnectPrompt : UserControl +{ + public ConnectPrompt() + { + InitializeComponent(); + + // Tunnelled, for the reason QuickConnect gives: the prompt owns Enter, Escape and Tab while the caret is + // in it, and a text box on the route must not get to them first. + AddHandler(KeyDownEvent, OnPromptKey, RoutingStrategies.Tunnel); + + // The password line appearing, or going, moves the caret with it: Enter on a name that needs a password + // should leave somebody typing the password, and Escape off it should leave them editing the name. + PasswordLine.PropertyChanged += OnLineChanged; + } + + /// The first line's box, for the caret and for the tests. + internal TextBox TargetBox => Target; + + /// The password line's box, for the caret and for the tests. + internal TextBox PasswordBox => Password; + + private MainWindowViewModel? Shell => DataContext as MainWindowViewModel; + + /// + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + // Posted, for the reason QuickConnect.TakeKeyboard gives at length: at the instant this becomes + // visible its boxes are not in the visual tree yet, and focus is refused to anything that is not. + if (change.Property == IsVisibleProperty && change.GetNewValue()) + { + Dispatcher.UIThread.Post(TakeKeyboard, DispatcherPriority.Loaded); + } + } + + private void OnLineChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == IsVisibleProperty && IsVisible) + { + Dispatcher.UIThread.Post(TakeKeyboard, DispatcherPriority.Loaded); + } + } + + /// Puts the caret on whichever line is asking. + /// + /// Both halves, for the reason gives: the terminal's WebView can still + /// hold the native window's focus after it has been collapsed, and an Avalonia focus without the native + /// call is a caret that receives nothing. + /// + private void TakeKeyboard() + { + if (!IsEffectivelyVisible) + { + return; + } + + if (TopLevel.GetTopLevel(this) is Window window) + { + NativeKeyboardFocus.ReturnTo(window); + } + + if (PasswordLine.IsVisible) + { + Password.Focus(); + } + else + { + Target.Focus(); + Target.CaretIndex = Target.Text?.Length ?? 0; + } + } + + private void OnPromptKey(object? sender, KeyEventArgs e) + { + if (Shell is not { IsConnectPromptShowing: true } shell || e.KeyModifiers != KeyModifiers.None) + { + return; + } + + switch (e.Key) + { + case Key.Enter: + // Fire-and-forget, as the palette's is: the command reports its own refusals on the prompt, + // and a connection's progress and failure in the tab it opens. + _ = shell.SubmitPromptCommand.ExecuteAsync(null); + e.Handled = true; + break; + + case Key.Escape: + shell.CancelPromptCommand.Execute(null); + e.Handled = true; + break; + + case Key.Tab: + // Only when there is something to complete to; otherwise Tab moves the focus, as it always does. + e.Handled = shell.CompletePrompt(); + + if (e.Handled) + { + Target.CaretIndex = Target.Text?.Length ?? 0; + } + + break; + + default: + break; + } + } +} diff --git a/src/DodoSSH.Client.App/Views/MainWindow.axaml b/src/DodoSSH.Client.App/Views/MainWindow.axaml index 115f189..976d17f 100644 --- a/src/DodoSSH.Client.App/Views/MainWindow.axaml +++ b/src/DodoSSH.Client.App/Views/MainWindow.axaml @@ -285,9 +285,8 @@ + +