Files
DodoSSH/src/DodoSSH.Client.App/Views/ConnectPrompt.axaml
T
jaap-jan 3c8c0980c2
ci / build and test (push) Successful in 4m13s
ci / android head (push) Successful in 4m24s
ci / api image (push) Successful in 13s
ci / deploy ssh.dodotech.cloud (push) Failing after 1s
ci / desktop nightly (push) Successful in 3m6s
Terminal: ask what to connect to when Terminal is pressed with nothing open
The terminal surface with no session used to be an empty frame, with the tab row's "+" the only
thing on it that did anything. It now holds a prompt, in the order ssh asks: "connect to:" takes a
saved host's name, matched exactly, with the hosts that match so far listed under it for Tab or a
click, or user@host[:port] for a machine that is not saved. A password line follows only when one
is needed, worded as ssh words it. Escape steps back a line; recent connections are listed below.

It is drawn by Avalonia in the WebView's rectangle, which the occlusion rule allows because the two
are exclusive: the prompt shows only while there are no tabs, the WebView only for a tab with a
session.

Saved hosts connect through a new VaultViewModel.ConnectToHostAsync, which the drawer's own connect
now goes through too, so every refusal is still made once. Each attempt now carries whether it may
act on the drawer's remember-password tick, and the prompt's may not: otherwise a tick left on in
a closed drawer would save a password typed at a prompt that never offered to keep anything.

Covered by nine ShellFlowTests cases and four ConnectPromptTests; manual check 7.10b.
2026-10-16 12:17:34 +02:00

114 lines
6.4 KiB
XML

<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:DodoSSH.Client.Shell.ViewModels"
xmlns:views="using:DodoSSH.Client.App.Views"
x:Class="DodoSSH.Client.App.Views.ConnectPrompt"
x:DataType="vm:MainWindowViewModel"
Background="{StaticResource Pane}">
<!--
What the terminal's rectangle holds when Terminal is pressed with nothing open: a prompt, because asking
is what a terminal with nothing in it does. It used to be an empty frame, with the tab row's "+" the only
thing on it that did anything.
Two lines, in the order ssh itself asks. The first takes a saved host's name — matched exactly, with the
names that match so far printed under it for Tab or a click to take — or user@host[:port] for a machine
that is not saved. The second appears only when a password is needed, worded as ssh words it, and is
never offered for a host whose keychain binding supplies its own credential.
Drawn by Avalonia in the WebView's own rectangle, which the occlusion rule allows only because the two
are exclusive: this shows while there are no tabs, and the WebView only for a selected tab with a
session. See MainWindowViewModel.IsConnectPromptShowing.
Everything it does goes through the vault's own connect paths — ConnectToHostAsync for a saved host,
ConnectToEndpointAsync for an address — so every refusal those make is made here too, and nothing typed
at this prompt is written to the keychain.
-->
<ScrollViewer HorizontalScrollBarVisibility="Disabled">
<StackPanel Margin="18,16" Spacing="3" TextElement.FontFamily="{StaticResource MonoFont}">
<TextBlock Classes="mono" FontSize="13" Foreground="{StaticResource TextFaint}" TextWrapping="Wrap"
Text="No session open. Connect to a saved host by name, or to any machine as user@host[:port]." />
<!-- ============ what to connect to ============ -->
<Grid ColumnDefinitions="Auto,*" Margin="0,14,0,0">
<TextBlock Grid.Column="0" Classes="mono" FontSize="13" FontWeight="Bold"
Foreground="{StaticResource AccentText}" Text="connect to:" VerticalAlignment="Center" />
<TextBox Grid.Column="1" x:Name="Target" Classes="prompt" Margin="8,0,0,0"
Text="{Binding PromptText}" IsReadOnly="{Binding IsPromptAskingForPassword}" />
</Grid>
<!--
The saved hosts that match so far, best first, the first one Tab's. A wrapped row rather than a list,
because this is a shell's completion line and not a picker: a name and an address, nothing to scroll.
-->
<ItemsControl ItemsSource="{Binding PromptMatches}" Margin="0,2,0,0"
IsVisible="{Binding ShowsPromptMatches}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:HostRowViewModel">
<Button Classes="flat" Focusable="False" Padding="6,2" Margin="0,0,6,0" CornerRadius="5"
Cursor="Hand" ToolTip.Tip="{Binding Address}"
Command="{Binding $parent[views:ConnectPrompt].((vm:MainWindowViewModel)DataContext).ChoosePromptMatchCommand}"
CommandParameter="{Binding}">
<TextBlock Classes="mono" FontSize="12" Foreground="{StaticResource TextDim}"
Text="{Binding Label}" />
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- ============ the password, when one is needed ============ -->
<Grid x:Name="PasswordLine" ColumnDefinitions="Auto,*" IsVisible="{Binding IsPromptAskingForPassword}">
<TextBlock Grid.Column="0" Classes="mono" FontSize="13" Foreground="{StaticResource Text}"
Text="{Binding PromptPasswordLabel}" VerticalAlignment="Center" />
<TextBox Grid.Column="1" x:Name="Password" Classes="prompt" Margin="8,0,0,0" PasswordChar="•"
Text="{Binding PromptPassword}" />
</Grid>
<TextBlock Classes="mono" FontSize="12.5" TextWrapping="Wrap" Margin="0,4,0,0"
Foreground="{StaticResource WarnText}"
Text="{Binding PromptRefusal}" IsVisible="{Binding HasPromptRefusal}" />
<TextBlock Classes="mono" FontSize="10.5" Margin="0,10,0,0" Foreground="{StaticResource TextGhost}"
Text="ENTER to connect · TAB to complete · ESC to go back · CTRL+K to search hosts" />
<!--
============ where you have been ============
The machines most recently connected to, newest first — the vault's own connection log, deduplicated
by address, so it survives a restart and arrives on a new machine with the keychain. A click puts the
row on the prompt line and carries on from there exactly as if it had been typed: a saved host
connects, or asks for its password; an address asks for its password.
-->
<StackPanel Margin="0,22,0,0" Spacing="1" IsVisible="{Binding HasRecentConnections}">
<TextBlock Classes="mono" FontSize="11" Foreground="{StaticResource TextFaint}" Text="recent"
Margin="0,0,0,4" />
<ItemsControl ItemsSource="{Binding RecentConnections}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:ConnectionLogRowViewModel">
<Button Classes="flat" Focusable="False" Padding="6,3" CornerRadius="5" Cursor="Hand"
HorizontalAlignment="Left"
Command="{Binding $parent[views:ConnectPrompt].((vm:MainWindowViewModel)DataContext).ChoosePromptRecentCommand}"
CommandParameter="{Binding}">
<StackPanel Orientation="Horizontal" Spacing="14">
<TextBlock Classes="mono" FontSize="12.5" Foreground="{StaticResource TextDim}"
Text="{Binding Address}" />
<TextBlock Classes="mono" FontSize="12" Foreground="{StaticResource TextFaint}"
Text="{Binding HostLabel}" />
</StackPanel>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</StackPanel>
</ScrollViewer>
</UserControl>