Files
DodoSSH/src/DodoSSH.Client.App/Views/PreferencesScreen.axaml
T
jaap-jan 2caedd93ff Merge branch 'main' into the Android head
Main grew the screens the host-management plan called for — hosts, pins, snippets, logs,
import, teams — plus the ObjectStore and Import projects behind two of them, and moved
WindowsDeviceKeyStore into the desktop head's Platform folder.

Five of those view models landed in a directory this branch had already moved, so they
join the rest in DodoSSH.Client.Shell: git spotted the rename and put them there, and the
namespaces followed. Shell picks up ObjectStore and Import as a result, which the Android
head then gets transitively and will use neither of at first — scoped storage means there
is no ~/.ssh/config to import, and file transfer is out of its first scope.

Desktop suites green at 155 and 64.
2026-07-31 21:03:22 +02:00

167 lines
10 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.PreferencesScreen"
x:DataType="vm:MainWindowViewModel">
<!--
Preferences.
The design's rail has six sections and its TERMINAL panel has six settings. None of the six settings can
be saved: there is no preferences table in the local cache, no preference item type in the vault, and no
channel from the host to the renderer that carries a terminal option — the data plane's frames are
output, session-opened, session-closed and session-activated, and nothing else. A font-size stepper
that reset on every launch would be worse than not having one.
So this screen ships what is real, which is not nothing: this machine's device key is a genuine
preference with a genuine effect, and it is the one thing on the design's SECURITY panel that exists.
The two commands behind it were already in the shell; they were merely homeless, wedged into the old
account bar because there was nowhere else to put them.
Everything else is listed as absent rather than omitted, because a preferences screen that is silent
about the settings it has not got reads as a product with six preferences.
-->
<ScrollViewer>
<StackPanel MaxWidth="620" Margin="28,26" HorizontalAlignment="Left">
<TextBlock Classes="mono" Text="THIS MACHINE" FontSize="13" FontWeight="SemiBold"
LetterSpacing="1" Foreground="{StaticResource Text}" />
<Grid ColumnDefinitions="*,Auto" Margin="0,12,0,0">
<StackPanel Grid.Column="0" Spacing="2" Margin="0,0,16,0">
<TextBlock Text="Unlock with Windows Hello" Foreground="{StaticResource Text}" FontSize="12"
FontWeight="Medium" />
<TextBlock Classes="hint" FontSize="10"
Text="Registers this machine so a later launch can open the keychain with a Windows confirmation instead of your passphrase. Your passphrase keeps working." />
</StackPanel>
<Button Grid.Column="1" Classes="accent" Content="REGISTER"
Command="{Binding RegisterDeviceCommand}"
IsEnabled="{Binding !IsBusy}"
IsVisible="{Binding CanRegisterDevice}" />
<!--
The withdrawal, in the place the offer was. Its own flag rather than the negation of that one: a
machine with no TPM and a machine that is already registered are both "cannot register", and only
the second has anything to take back.
-->
<Button Grid.Column="1" Classes="danger" Content="STOP UNLOCKING HERE"
Command="{Binding ForgetDeviceCommand}"
IsEnabled="{Binding !IsBusy}"
IsVisible="{Binding CanForgetDevice}"
ToolTip.Tip="Withdraws this machine's device key, here and from your account, so it goes back to asking for your passphrase. Do this to a machine you have lost." />
</Grid>
<!-- Neither flag is set on a machine that cannot keep a key at all, and that is worth saying. -->
<TextBlock Classes="hint" FontSize="10" Margin="0,8,0,0"
Text="This machine has nowhere to keep a device key, so the keychain will keep asking for your passphrase. That needs a TPM and a Windows keystore willing to release the key."
IsVisible="{Binding HasNoDeviceKeyOption}" />
<Border Height="1" Background="{StaticResource BorderSubtle}" Margin="0,20" />
<TextBlock Classes="mono" Text="KEYCHAIN" FontSize="13" FontWeight="SemiBold"
LetterSpacing="1" Foreground="{StaticResource Text}" />
<Grid ColumnDefinitions="*,Auto" Margin="0,12,0,0">
<StackPanel Grid.Column="0" Spacing="2" Margin="0,0,16,0">
<TextBlock Text="Lock the keychain" Foreground="{StaticResource Text}" FontSize="12"
FontWeight="Medium" />
<TextBlock Classes="hint" FontSize="10"
Text="Closes the keychain and forgets every key it held. Shells you have open keep running and reappear when you unlock — locked describes the keychain, not this machine's access to your hosts." />
</StackPanel>
<Button Grid.Column="1" Classes="ghost" Content="LOCK NOW" Command="{Binding LockCommand}" />
</Grid>
<Grid ColumnDefinitions="*,Auto" Margin="0,14,0,0">
<StackPanel Grid.Column="0" Spacing="2" Margin="0,0,16,0">
<TextBlock Text="Synchronise" Foreground="{StaticResource Text}" FontSize="12"
FontWeight="Medium" />
<TextBlock Classes="hint" FontSize="10"
Text="Runs a pass now. One runs on its own when the keychain opens, straight after any change, and every minute while it stays open — and a pass that finds this machine offline signs it back in from the session it remembered, so nothing here depends on being pressed." />
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6">
<Button Classes="ghost" Content="SIGN IN" Command="{Binding SignInCommand}"
IsVisible="{Binding !IsOnline}"
ToolTip.Tip="Opens your browser. Only needed when there is no remembered session to resume — after signing out, or once your identity provider stops accepting the one this machine held." />
<Button Classes="ghost" Content="SYNC NOW" Command="{Binding Vault.SyncCommand}" />
</StackPanel>
</Grid>
<Grid ColumnDefinitions="*,Auto" Margin="0,14,0,0">
<StackPanel Grid.Column="0" Spacing="2" Margin="0,0,16,0">
<TextBlock Text="Import from ~/.ssh/config" Foreground="{StaticResource Text}" FontSize="12"
FontWeight="Medium" />
<TextBlock Classes="hint" FontSize="10"
Text="Reads this machine's OpenSSH configuration and offers what it finds. It shows you the list first and stores nothing until you say so, and it does not read any private key — where a key file is named, the path is recorded as a note." />
</StackPanel>
<Button Grid.Column="1" Classes="ghost" Content="IMPORT HOSTS"
Command="{Binding ShowScreenCommand}"
CommandParameter="{x:Static vm:ShellScreen.Import}" />
</Grid>
<Border Height="1" Background="{StaticResource BorderSubtle}" Margin="0,20" />
<TextBlock Classes="mono" Text="ACCOUNT" FontSize="13" FontWeight="SemiBold"
LetterSpacing="1" Foreground="{StaticResource Text}" />
<TextBlock Classes="mono" Text="{Binding AccountName}" FontSize="11" Margin="0,8,0,0"
Foreground="{StaticResource Info}" TextTrimming="CharacterEllipsis" />
<Grid ColumnDefinitions="*,Auto" Margin="0,12,0,0">
<StackPanel Grid.Column="0" Spacing="2" Margin="0,0,16,0">
<TextBlock Text="Sign out of this machine" Foreground="{StaticResource Text}" FontSize="12"
FontWeight="Medium" />
<TextBlock Classes="hint" FontSize="10"
Text="Deletes this machine's copy of the keychain and withdraws its device key, so it goes back to knowing nothing. The keychain stays on the server; signing in again brings it back. Use this to hand a machine on, or to enrol a different account." />
</StackPanel>
<!--
Hidden rather than disabled while the confirmation is up, because the card below carries the
button that actually does it and two sign-out buttons on one screen is one too many.
-->
<Button Grid.Column="1" Classes="danger" Content="SIGN OUT"
Command="{Binding SignOutCommand}"
IsEnabled="{Binding !IsBusy}"
IsVisible="{Binding !IsConfirmingSignOut}" />
</Grid>
<Border Background="{StaticResource Panel}" BorderBrush="{StaticResource Border}"
BorderThickness="1" CornerRadius="6" Padding="14" Margin="0,14,0,0"
IsVisible="{Binding IsConfirmingSignOut}">
<views:SignOutCard />
</Border>
<Border Height="1" Background="{StaticResource BorderSubtle}" Margin="0,20" />
<TextBlock Classes="mono" Text="NOT BUILT YET" FontSize="13" FontWeight="SemiBold"
LetterSpacing="1" Foreground="{StaticResource TextDim}" />
<TextBlock Classes="hint" FontSize="11" Margin="0,8,0,0"
Text="These are on the design and have nothing behind them. They are listed rather than left out, so that what this screen does not do is as legible as what it does. The full list, and what each would take, is in docs/design-import-gaps.md." />
<ItemsControl Margin="0,12,0,0">
<ItemsControl.Styles>
<Style Selector="TextBlock.gap">
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
<Setter Property="FontSize" Value="11" />
<Setter Property="TextWrapping" Value="Wrap" />
<Setter Property="Margin" Value="0,0,0,7" />
</Style>
</ItemsControl.Styles>
<TextBlock Classes="gap"
Text="Terminal font, size, cursor and scrollback — the renderer hard-codes them, and nothing carries a change to it." />
<TextBlock Classes="gap"
Text="Any preference at all, saved — there is no preferences store in the local cache and no preference item type in the keychain." />
<TextBlock Classes="gap"
Text="Auto-lock after idle — nothing tracks idleness, and the lock policy would have to decide what to do about a shell mid-job." />
<TextBlock Classes="gap"
Text="Per-use approval before a key signs — keys are handed to the SSH stack whole at connect time, so there is no per-signature moment to interrupt." />
<TextBlock Classes="gap"
Text="SSO and team policy — the server has no team endpoints, so there is no policy for this screen to show." />
<TextBlock Classes="gap"
Text="Keyboard shortcuts — the window binds one chord, and the terminal keeps the rest for the remote." />
</ItemsControl>
</StackPanel>
</ScrollViewer>
</UserControl>