Give DodoSSH a phone, and a shared shell for both heads to drive

The Android head from docs/android-port.md, taken as far as its step 6.

Step 3, the spike, is answered and its throwaway screen is gone: libsodium.so and
libe_sqlite3.so are both in the arm64 APK, so NSec resolves its native half on Android
despite shipping no Android build, and the local cache opens. Two findings the audit
could not have had: Avalonia.Controls.WebView only ships net10.0-android36.0, which
settles the open "which Android versions" question at targetSdk 36; and Android has
blocked cleartext HTTP since API 28, so the terminal renderer needs a network security
config scoped to 127.0.0.1 or the WebView loads nothing.

DodoSSH.Client.Shell is new and is why the phone can exist: the view models, the terminal
renderer files and the palette moved there so both heads drive one state machine and draw
from one set of tokens. The desktop head is otherwise untouched and its 144 tests still
pass.

The platform pieces behind interfaces that already existed: the profile directory from
filesDir, a device key wrapped by a StrongBox-backed key that a fingerprint releases, and
a foreground service so a shell outliving a vault lock stays true on a platform that
stops backgrounded processes.

Sign-in is deliberately absent rather than approximated. It needs an app link, because
reusing the desktop loopback listener is the attack RFC 8252 section 8.3 names.
This commit is contained in:
2026-07-31 20:58:48 +02:00
parent 03e902a2d2
commit fe9d7fc289
65 changed files with 3034 additions and 103 deletions
@@ -0,0 +1,81 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!--
The palette, named for what a colour is for rather than for what it looks like. Every one of these is
from the design; the names are this codebase's, because "#0C0F0E" appearing in nine files is how a
surface ends up two shades off in the tenth.
It lives here, in the shared project, because there are now two heads drawing the same product. Two
copies of a palette is the same failure one file up: the phone's "connected" green drifting from the
desktop's is not a thing anybody would notice until a screenshot sat beside another screenshot.
Five near-black surfaces rather than one, and the difference between them is real work: the window is
the darkest so the terminal reads as the lit thing, chrome sits one step up so the header and status
bar frame it, and the lists sit between the two so a list does not look like part of either.
-->
<Color x:Key="CanvasColor">#0A0C0B</Color>
<SolidColorBrush x:Key="Canvas" Color="{StaticResource CanvasColor}" />
<SolidColorBrush x:Key="Chrome" Color="#0D100F" />
<SolidColorBrush x:Key="Sidebar" Color="#0C0F0E" />
<SolidColorBrush x:Key="Panel" Color="#0F1211" />
<SolidColorBrush x:Key="Raised" Color="#111514" />
<SolidColorBrush x:Key="Field" Color="#121615" />
<!-- Row hover, and the heavier one the chrome's own buttons use. On the phone these are press states. -->
<SolidColorBrush x:Key="Hover" Color="#141817" />
<SolidColorBrush x:Key="ChromeHover" Color="#1A1F1D" />
<!--
Three border weights, and they are not interchangeable. Strong separates one region from another,
subtle separates rows inside one region, and mid is what a control draws around itself.
-->
<SolidColorBrush x:Key="Border" Color="#1E2422" />
<SolidColorBrush x:Key="BorderSubtle" Color="#171C1A" />
<SolidColorBrush x:Key="BorderMid" Color="#2A312E" />
<SolidColorBrush x:Key="BorderHover" Color="#3A423E" />
<SolidColorBrush x:Key="BorderFaint" Color="#232927" />
<!--
The text ramp. Three steps, used consistently: what you read, what you glance at, and what is there
only so its absence would be noticed. A fourth step would be one nobody could tell from its neighbours.
-->
<SolidColorBrush x:Key="Text" Color="#DCE3DF" />
<SolidColorBrush x:Key="TextDim" Color="#7E8A84" />
<SolidColorBrush x:Key="TextFaint" Color="#566059" />
<SolidColorBrush x:Key="TextGhost" Color="#404743" />
<!--
The accent, and the three colours that are allowed to disagree with it. Green means live, connected or
yours; amber means a caveat worth reading; red means refused or destructive; blue is for the one thing
that is neither — a directory, a distinct scope — and is deliberately rare.
-->
<Color x:Key="AccentColor">#3CE88F</Color>
<SolidColorBrush x:Key="Accent" Color="{StaticResource AccentColor}" />
<SolidColorBrush x:Key="AccentSoft" Color="#3CE88F" Opacity="0.35" />
<SolidColorBrush x:Key="AccentWash" Color="#3CE88F" Opacity="0.06" />
<SolidColorBrush x:Key="Warn" Color="#E8B44C" />
<SolidColorBrush x:Key="WarnSoft" Color="#E8B44C" Opacity="0.35" />
<SolidColorBrush x:Key="WarnWash" Color="#E8B44C" Opacity="0.06" />
<SolidColorBrush x:Key="WarnText" Color="#B9A26B" />
<SolidColorBrush x:Key="Danger" Color="#E85D5D" />
<SolidColorBrush x:Key="DangerSoft" Color="#E85D5D" Opacity="0.3" />
<SolidColorBrush x:Key="DangerWash" Color="#E85D5D" Opacity="0.08" />
<SolidColorBrush x:Key="DangerText" Color="#D98A8A" />
<SolidColorBrush x:Key="Info" Color="#5DA9E8" />
<!--
The design asks for IBM Plex Mono and IBM Plex Sans. Neither ships with this application, and neither
is on a stock Windows install or a stock Android one, so requesting them by name would render as
whatever the font fallback chose that day — which is worse than choosing deliberately. Inter is
embedded by both heads and is what they already draw with.
The stack ends in the generic `monospace` rather than a Windows face, which is what makes it work on
both: Android has no Cascadia Mono or Consolas and resolves the generic name to its own mono face.
Named as a resource rather than repeated, because the substitution is the sort of thing that gets
reversed later and should be reversible in one place. See docs/design-import-gaps.md.
-->
<FontFamily x:Key="MonoFont">ui-monospace,Cascadia Mono,Consolas,monospace</FontFamily>
</ResourceDictionary>