Public Access
An audit of README.md, the seven docs and the fourteen ADRs against the code, looking for what is described as absent or planned. Most of it held. What did not is here, and it clusters: every stale claim but one is downstream of the settings file arriving without this document noticing. design-import-gaps said the client has no preferences store and writes exactly two files. It writes three — ClientSettings is in settings.json beside the cache — and two preferences are saved through it. From that one error followed four more: the terminal font size row said "fixed at the renderer's 13px" when it has been 8 to 32 from a screen and three chords for some time; the transfer-resume row and the per-host last directory row both blamed a store that now exists, when what they actually want is a table and a scalar file is the wrong shape for one; and the Preferences table asserted no preference could be saved at all. It also said TerminalServerOpcode has four values and none carries an option. It has eight, and one of them is FontSize — which is the interesting part rather than a counting error, because that opcode is the proof that the frame these rows say is missing can be built. The rows now say what each one would actually take, which for three of the four is a setting, an opcode and a control, and for the Backspace row is a reason: which byte backspace sends is a fact about the remote's stty, so a client-side switch fixes a mismatch by hiding it. THE SAME TWO ERRORS WERE SHIPPED IN THE INTERFACE. The preferences screen carries a NOT BUILT YET list, so that what the screen does not do is as legible as what it does — and it said terminal size was hard-coded a hundred lines below a working size control, and said there is one release channel a month after the nightly shipped. A list of absences is only worth having if it is true, and a screen contradicting itself in the same scroll is worse than no list. Both lines are corrected rather than removed: the first now says which three of the four are genuinely hard-coded, and the second says what is actually missing, which is a way to change channel from inside the application rather than by installing the other build. docs/adding-hosts-on-the-phone.md is deleted. It was a work plan whose own header says "Status: built. All six steps." — nothing links to it, and the decisions it records are in the code it produced, including the one it is proudest of: HostSecret.AsksForPassword carries its own remark on why naming neither binding had to stop meaning "ask me". What was left was step ordering and per-test instructions for work that shipped. Git keeps it. crypto.md is deliberately untouched. It is normative and frozen, and its claims are about the DSH1 format rather than about this build — including the one that reads oddly next to the code, that a passphrase is one of four ways to open a vault. Under the spec it is. What is missing is a statement about what this build can open, and that belongs beside the spec rather than inside it; see docs/unlocking-without-the-passphrase.md. VERIFIED. Build clean, 112 layout tests, 354 app tests. The preferences screen is measured by the layout suite, so the longer copy is held inside the window at the minimum size rather than assumed to fit.
276 lines
18 KiB
XML
276 lines
18 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. One of those six is now
|
||
real: text size. It needed all three of the things this comment used to record as missing — somewhere
|
||
to keep a preference (settings.json beside the cache, outside it deliberately, so it can be read on a
|
||
launch that never unlocks anything), a frame carrying a terminal option (TerminalServerOpcode.FontSize),
|
||
and a way for the page's own chords to reach the host that owns the value
|
||
(TerminalClientOpcode.FontSizeStep). The rule that kept it off this screen until then still stands and
|
||
is why the storage came first: a stepper that reset on every launch is worse than no stepper.
|
||
|
||
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="14" 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="13"
|
||
FontWeight="Medium" />
|
||
<TextBlock Classes="hint" FontSize="11"
|
||
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="11" 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="UPDATES" FontSize="14" FontWeight="SemiBold"
|
||
LetterSpacing="1" Foreground="{StaticResource Text}" />
|
||
|
||
<!--
|
||
Not on the design at all, unlike everything else here. It arrived with packaging: an installed
|
||
client can replace itself, and the moment that is true the question of where a replacement comes
|
||
from stops being theoretical. The answer is the security content of this section rather than a
|
||
footnote to it, which is why it is printed under the version instead of hidden in a tooltip.
|
||
-->
|
||
<TextBlock Classes="mono" Text="{Binding Updates.CurrentVersion}" FontSize="12" Margin="0,8,0,0"
|
||
Foreground="{StaticResource Info}" TextTrimming="CharacterEllipsis" />
|
||
<TextBlock Classes="hint" FontSize="11" Margin="0,4,0,0"
|
||
Text="Builds come from the project's own release page, and never from the server you sign in to. That is deliberate: whoever hands you the client can hand you a client that copies your passphrase, and the operator of a DodoSSH deployment is the party the trust model is about. A deployment may tell you where to get it. It is not where it comes from." />
|
||
|
||
<Grid ColumnDefinitions="*,Auto" Margin="0,14,0,0">
|
||
<StackPanel Grid.Column="0" Spacing="2" Margin="0,0,16,0">
|
||
<TextBlock Text="Check for updates" Foreground="{StaticResource Text}" FontSize="13"
|
||
FontWeight="Medium" />
|
||
<TextBlock Classes="hint" FontSize="11"
|
||
Text="Asks the release page whether there is a newer build, and downloads it if there is. Nothing is ever installed while you are using it — a downloaded update waits for a restart you ask for, or for the next time you start DodoSSH." />
|
||
</StackPanel>
|
||
<Button x:Name="CheckNowButton" Grid.Column="1" Classes="ghost" Content="CHECK NOW"
|
||
Command="{Binding Updates.CheckNowCommand}"
|
||
IsEnabled="{Binding Updates.CanCheckNow}" />
|
||
</Grid>
|
||
|
||
<Grid ColumnDefinitions="*,Auto" Margin="0,14,0,0">
|
||
<StackPanel Grid.Column="0" Spacing="2" Margin="0,0,16,0">
|
||
<TextBlock Text="Check on its own" Foreground="{StaticResource Text}" FontSize="13"
|
||
FontWeight="Medium" />
|
||
<TextBlock Classes="hint" FontSize="11"
|
||
Text="Every six hours while DodoSSH is running, starting a couple of minutes after launch. It keeps checking while the keychain is locked, because where builds come from has nothing to do with your vault." />
|
||
</StackPanel>
|
||
<CheckBox x:Name="AutomaticUpdatesToggle" Grid.Column="1" VerticalAlignment="Top"
|
||
IsChecked="{Binding Updates.IsAutomatic}"
|
||
IsEnabled="{Binding Updates.IsSupported}" />
|
||
</Grid>
|
||
|
||
<!-- The only other ProgressBar in the application is the transfers one; same height, same brushes. -->
|
||
<ProgressBar Height="4" Minimum="0" Maximum="100" Margin="0,12,0,0"
|
||
Value="{Binding Updates.DownloadPercent}"
|
||
Foreground="{StaticResource Accent}" Background="{StaticResource Raised}"
|
||
IsVisible="{Binding Updates.IsDownloading}" />
|
||
|
||
<!--
|
||
The restart, with the sentence the banner only has room for in a tooltip. This screen scrolls, so
|
||
this is where the warning can be as long as it needs to be — and it needs to be, because this
|
||
application has spent a lot of words teaching that locking keeps shells running.
|
||
-->
|
||
<Grid ColumnDefinitions="*,Auto" Margin="0,14,0,0" IsVisible="{Binding Updates.IsReady}">
|
||
<StackPanel Grid.Column="0" Spacing="2" Margin="0,0,16,0">
|
||
<TextBlock Text="{Binding Updates.ReadyHeadline}" Foreground="{StaticResource Text}"
|
||
FontSize="13" FontWeight="Medium" TextWrapping="Wrap" />
|
||
<TextBlock Classes="hint" FontSize="11" Text="{Binding Updates.RestartWarning}" />
|
||
</StackPanel>
|
||
<Button Grid.Column="1" Classes="accent" Content="RESTART NOW"
|
||
Command="{Binding Updates.RestartNowCommand}" />
|
||
</Grid>
|
||
|
||
<TextBlock Classes="hint" FontSize="11" Margin="0,8,0,0"
|
||
Text="{Binding Updates.Status}"
|
||
IsVisible="{Binding Updates.Status, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||
|
||
<!--
|
||
The HasNoDeviceKeyOption precedent, one section up: a machine that gets none of the above is told
|
||
why rather than shown three controls that cannot do anything.
|
||
-->
|
||
<TextBlock Classes="hint" FontSize="11" Margin="0,8,0,0"
|
||
Text="This copy of DodoSSH cannot replace itself, so none of the above does anything. That is what a build run from a source checkout looks like, and also what a copy somebody unzipped by hand looks like — it is the installer that registers the update path."
|
||
IsVisible="{Binding Updates.IsUnsupported}" />
|
||
|
||
<Border Height="1" Background="{StaticResource BorderSubtle}" Margin="0,20" />
|
||
|
||
<TextBlock Classes="mono" Text="TERMINAL" FontSize="14" FontWeight="SemiBold"
|
||
LetterSpacing="1" Foreground="{StaticResource Text}" />
|
||
|
||
<!--
|
||
Here as well as on the chord, and not because the chord is in doubt. Ctrl+plus can only be heard
|
||
while a terminal has focus, since that is where the keyboard is being read — so somebody who has
|
||
not opened one yet, or who has just made the text too small to find anything in, has nowhere else
|
||
to look. This is that place, and it names the chord so the screen teaches it rather than replacing
|
||
it.
|
||
-->
|
||
<Grid ColumnDefinitions="*,Auto" Margin="0,12,0,0">
|
||
<StackPanel Grid.Column="0" Spacing="2" Margin="0,0,16,0">
|
||
<TextBlock Text="Text size" Foreground="{StaticResource Text}" FontSize="13"
|
||
FontWeight="Medium" />
|
||
<TextBlock Classes="hint" FontSize="11"
|
||
Text="How large a terminal draws, in pixels. Ctrl+plus and Ctrl+minus do the same while a terminal has focus, and Ctrl+0 puts it back. It resizes the grid rather than magnifying it, so the remote is told how many columns it now has — which is also why it stops before the columns run out." />
|
||
</StackPanel>
|
||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6" VerticalAlignment="Top">
|
||
<Button Classes="ghost" Content="A−" Command="{Binding ShrinkTerminalFontCommand}"
|
||
IsEnabled="{Binding CanShrinkTerminalFont}"
|
||
ToolTip.Tip="Smaller · Ctrl+minus" />
|
||
<TextBlock Classes="mono" FontSize="13" MinWidth="26" VerticalAlignment="Center"
|
||
TextAlignment="Center" Foreground="{StaticResource Text}"
|
||
Text="{Binding TerminalFontSize}" />
|
||
<Button Classes="ghost" Content="A+" Command="{Binding EnlargeTerminalFontCommand}"
|
||
IsEnabled="{Binding CanEnlargeTerminalFont}"
|
||
ToolTip.Tip="Larger · Ctrl+plus" />
|
||
<Button Classes="ghost" Content="RESET" Command="{Binding ResetTerminalFontCommand}"
|
||
ToolTip.Tip="Back to the size it ships at · Ctrl+0" />
|
||
</StackPanel>
|
||
</Grid>
|
||
|
||
<Border Height="1" Background="{StaticResource BorderSubtle}" Margin="0,20" />
|
||
|
||
<TextBlock Classes="mono" Text="KEYCHAIN" FontSize="14" 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="13"
|
||
FontWeight="Medium" />
|
||
<TextBlock Classes="hint" FontSize="11"
|
||
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="13"
|
||
FontWeight="Medium" />
|
||
<TextBlock Classes="hint" FontSize="11"
|
||
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="13"
|
||
FontWeight="Medium" />
|
||
<TextBlock Classes="hint" FontSize="11"
|
||
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="14" FontWeight="SemiBold"
|
||
LetterSpacing="1" Foreground="{StaticResource Text}" />
|
||
|
||
<TextBlock Classes="mono" Text="{Binding AccountName}" FontSize="12" 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="13"
|
||
FontWeight="Medium" />
|
||
<TextBlock Classes="hint" FontSize="11"
|
||
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="14" FontWeight="SemiBold"
|
||
LetterSpacing="1" Foreground="{StaticResource TextDim}" />
|
||
<TextBlock Classes="hint" FontSize="12" 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, cursor and scrollback — the renderer hard-codes those three. The text size above is the one that is not, and it is the path the others would follow if they were worth a row here." />
|
||
<TextBlock Classes="gap"
|
||
Text="Changing channel from in here — there is a nightly as well as a release, but which one a copy follows is fixed when it is built, so moving between them means installing the other one." />
|
||
<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 organisation policy — the server has endpoints for membership and none for policy, so there is nothing 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>
|