Public Access
Give the phone the rest of its screens, and a way in
All seven screens of the design, plus the two it does not draw because it starts at an enrolled phone: naming a server, and choosing a passphrase. The five states docs/android-port.md worried about losing at 360dp are all here and none of them softened. The changed-key refusal is a full-screen panel rather than a bottom sheet, because a sheet is swipe-to-dismiss by convention and that screen must have no way forward. The recovery code raises FLAG_SECURE for its own state and lowers it afterwards, so the sentence about screenshots is true rather than decorative. The delete confirmations keep their counts and replace the row in place. Signing in works, and the seam it needed is worth more than the implementation: IAuthorizationCallback now sits between OidcClient and the loopback listener, so the two heads differ in where the response arrives and in nothing else. PKCE, the state check, discovery, the token exchange and the key binding stay one implementation — a second OIDC client would be a second place for a security bug to live. The phone registers a private-use scheme with the system rather than binding a loopback port, which on a shared device any other app can do first. The accessory key row needed TerminalWorkspace.SendInputAsync: ordinary typing goes from the renderer straight down the socket, and there was no way in for the keys a software keyboard does not have. Ctrl latches, because one thumb cannot chord, and the latch is drawn — a modifier that is on and does not look on is how somebody sends ^L to a database prompt believing they typed an l. 597 client tests green, including two new ones for the input path and one for the terminal surface command. Nothing has run on a device.
This commit is contained in:
@@ -7,51 +7,153 @@
|
||||
Background="{StaticResource Canvas}">
|
||||
|
||||
<!--
|
||||
The phone's single view, and the counterpart of the desktop head's MainWindow — except that this one
|
||||
has no window, no nav rail, no titlebar and no status bar. It switches on ShellState and nothing else.
|
||||
The phone's single view, and the counterpart of the desktop head's MainWindow — except that this one has
|
||||
no window, no nav rail, no titlebar and no status bar.
|
||||
|
||||
The states are the same six the desktop has, and they are the same six for a good reason: they are the
|
||||
shell's state machine, which both heads share. What differs is only what each one draws.
|
||||
It switches on ShellState, which is the same six states the desktop has because both heads share one
|
||||
state machine. What differs is only what each one draws.
|
||||
|
||||
Panels rather than a template selector, matching the desktop head: each screen's visibility is one
|
||||
binding, and the whole tree is laid out once. There is no WebView occlusion problem to design around
|
||||
here, which is the one structural simplification the phone gets for free — see docs/android-port.md §9
|
||||
for what is still unverified about that on this platform.
|
||||
The nav rail's eight destinations become four in a bottom bar. That is not a reflow: the plan says five
|
||||
characters was a desktop constraint and the phone can use words, and it also says a phone cannot carry
|
||||
eight. What is here is what the decided scope needs — the keychain and a terminal — and the destinations
|
||||
behind the other four are reachable from those two rather than being hidden.
|
||||
-->
|
||||
|
||||
<Panel>
|
||||
|
||||
<views:LockedScreen IsVisible="{Binding IsLocked}" DataContext="{Binding}" />
|
||||
|
||||
<!--
|
||||
The states this head has not built yet, named rather than hidden. The convention is the desktop
|
||||
head's NotBuiltScreen and the reason is in README: nothing is rendered with invented data to fill a
|
||||
screen, and a state that silently showed nothing would be indistinguishable from one that had
|
||||
quietly broken.
|
||||
|
||||
Sign-in is the substantial one, and it is not merely unwritten — it needs a different redirect. See
|
||||
docs/android-port.md §5: the loopback listener the desktop uses is the attack RFC 8252 §8.3 names on
|
||||
a shared device, so this head needs an app link before it can honestly offer the flow at all.
|
||||
-->
|
||||
<!-- ============ getting in ============ -->
|
||||
<views:PendingScreen IsVisible="{Binding IsStarting}"
|
||||
Heading="OPENING THE KEYCHAIN"
|
||||
Detail="Reading this phone's local cache to find out whether it is enrolled." />
|
||||
|
||||
<views:PendingScreen IsVisible="{Binding IsNeedingServer}"
|
||||
Heading="SIGN-IN IS NOT BUILT HERE YET"
|
||||
Detail="This phone has no profile, and signing in needs a redirect this head does not have. The desktop client's loopback listener is deliberately not reused: on a shared device any other app can bind a loopback port, which is the attack RFC 8252 §8.3 names. An app link is the next piece of work. Enroll on the desktop client and this phone will unlock against the same vault." />
|
||||
<views:ServerScreen IsVisible="{Binding IsNeedingServer}" />
|
||||
<views:EnrollScreen IsVisible="{Binding IsNeedingEnrollment}" />
|
||||
<views:RecoveryCodeScreen IsVisible="{Binding IsShowingRecoveryCode}" />
|
||||
<views:LockedScreen IsVisible="{Binding IsLocked}" />
|
||||
|
||||
<views:PendingScreen IsVisible="{Binding IsNeedingEnrollment}"
|
||||
Heading="ENROLLMENT IS NOT BUILT HERE YET"
|
||||
Detail="This account has no vault key. Choosing a passphrase — and writing down the recovery code that follows it — happens on the desktop client for now." />
|
||||
<!-- ============ unlocked ============ -->
|
||||
<Grid IsVisible="{Binding IsUnlocked}" RowDefinitions="Auto,*,Auto,Auto">
|
||||
|
||||
<views:PendingScreen IsVisible="{Binding IsShowingRecoveryCode}"
|
||||
Heading="RECOVERY CODE"
|
||||
Detail="This state is reachable only after enrollment, which this head does not do yet. It is the one screen a user must never be able to click past, so it is left unbuilt rather than approximated." />
|
||||
<!-- The header: which vault, and whether it is synced. -->
|
||||
<Border Grid.Row="0" Background="{StaticResource Chrome}" BorderBrush="{StaticResource Border}"
|
||||
BorderThickness="0,0,0,1" Padding="16,0" Height="52">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<Border Grid.Column="0" Width="20" Height="20" BorderBrush="{StaticResource Accent}"
|
||||
BorderThickness="1" VerticalAlignment="Center">
|
||||
<TextBlock Text=">_" Foreground="{StaticResource Accent}"
|
||||
FontFamily="{StaticResource MonoFont}" FontSize="9" FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
|
||||
<views:PendingScreen IsVisible="{Binding IsUnlocked}"
|
||||
Heading="UNLOCKED"
|
||||
Detail="The vault is open. The host list, the keychain and the terminal are the next tranche of screens; the view models behind all three are already here and already driven by the desktop head." />
|
||||
<StackPanel Grid.Column="1" Margin="10,0,0,0" VerticalAlignment="Center" Spacing="1">
|
||||
<TextBlock Classes="mono" FontSize="12" FontWeight="SemiBold"
|
||||
Text="{Binding Vault.VaultName}" TextTrimming="CharacterEllipsis" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||
<Ellipse Classes="dot" Classes.live="{Binding IsFullySynced}" Width="5" Height="5"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Classes="label" FontSize="8" Text="{Binding SyncLabel}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<Button Grid.Column="2" Classes="row" Width="44" MinHeight="44" Padding="0"
|
||||
HorizontalContentAlignment="Center" Command="{Binding LockCommand}">
|
||||
<TextBlock Text="LOCK" Classes="label" FontSize="8.5" Foreground="{StaticResource TextDim}" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- The screens. Only one draws; which one is the shell's business. -->
|
||||
<Panel Grid.Row="1">
|
||||
<!--
|
||||
Wrapped rather than bound directly, and the desktop head's MainWindow carries the same note for
|
||||
the same reason: this element's visibility is the shell's business and its data context is the
|
||||
vault, and putting both on one element resolves IsVisible against the vault — where
|
||||
IsHostsShowing does not exist. It is a compile error here rather than a silent blank screen only
|
||||
because compiled bindings are on.
|
||||
-->
|
||||
<Panel IsVisible="{Binding IsHostsShowing}">
|
||||
<views:HostsScreen DataContext="{Binding Vault}" />
|
||||
</Panel>
|
||||
<Panel IsVisible="{Binding IsVaultShowing}">
|
||||
<views:KeychainScreen DataContext="{Binding Vault}" />
|
||||
</Panel>
|
||||
<views:TerminalScreen IsVisible="{Binding IsTerminalShowing}" />
|
||||
|
||||
<views:PendingScreen IsVisible="{Binding IsPreferencesShowing}"
|
||||
Heading="PREFERENCES"
|
||||
Detail="Registering this phone's device key, and signing out, are on the unlock screen for now. Importing an ~/.ssh/config has no meaning here — scoped storage means there is no such file to read." />
|
||||
</Panel>
|
||||
|
||||
<!--
|
||||
◆ The two host-key decisions, over everything. Outside the screen Panel deliberately: file transfer
|
||||
makes its own trust decision on the desktop, and when the phone grows that screen this must already
|
||||
be somewhere both can reach rather than nested inside one of them.
|
||||
-->
|
||||
<Panel Grid.Row="1">
|
||||
<views:HostKeySheet DataContext="{Binding Vault}" />
|
||||
</Panel>
|
||||
|
||||
<!--
|
||||
The shells strip. The design puts it above the nav on every screen, and that is the single most
|
||||
desktop-shaped idea in the product — it is what makes a terminal a surface you switch to rather than
|
||||
a screen you navigate away from. Kept, because losing it would make an open shell invisible from the
|
||||
host list, which is where you are when you open the next one.
|
||||
-->
|
||||
<Border Grid.Row="2" IsVisible="{Binding HasTabs}" Background="{StaticResource Sidebar}"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="0,1,0,0" Height="46">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8" Margin="12,0" VerticalAlignment="Center">
|
||||
<TextBlock Classes="label" FontSize="8" Text="SHELLS" VerticalAlignment="Center" />
|
||||
<ItemsControl ItemsSource="{Binding Tabs}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate><StackPanel Orientation="Horizontal" Spacing="6" /></ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:TerminalTabViewModel">
|
||||
<Button Classes="row" MinHeight="32" Padding="10,6" CornerRadius="5"
|
||||
Background="{StaticResource Field}"
|
||||
Command="{Binding $parent[views:PhoneShell].((vm:MainWindowViewModel)DataContext).SelectTabCommand}"
|
||||
CommandParameter="{Binding}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Ellipse Classes="dot live" Width="5" Height="5" VerticalAlignment="Center" />
|
||||
<TextBlock Classes="mono" FontSize="10" Text="{Binding Label}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<!-- ============ navigation ============ -->
|
||||
<Border Grid.Row="3" Background="{StaticResource Chrome}" BorderBrush="{StaticResource Border}"
|
||||
BorderThickness="0,1,0,0" Height="58">
|
||||
<Grid ColumnDefinitions="*,*,*,*">
|
||||
|
||||
<views:NavButton Grid.Column="0" Label="HOSTS" IsCurrent="{Binding IsHostsShowing}"
|
||||
Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.Hosts}" />
|
||||
|
||||
<views:NavButton Grid.Column="1" Label="KEYCHAIN" IsCurrent="{Binding IsVaultShowing}"
|
||||
Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.Vault}" />
|
||||
|
||||
<!--
|
||||
The terminal is a surface rather than a page — see ShellSurface — so this one does not go
|
||||
through ShowScreen. Its own command is on the shell.
|
||||
-->
|
||||
<views:NavButton Grid.Column="2" Label="TERMINAL" IsCurrent="{Binding IsTerminalShowing}"
|
||||
Command="{Binding ShowTerminalCommand}" />
|
||||
|
||||
<views:NavButton Grid.Column="3" Label="MORE" IsCurrent="{Binding IsPreferencesShowing}"
|
||||
Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.Preferences}" />
|
||||
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
|
||||
</Panel>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user