Public Access
A terminal whose machine rebooted stayed green for ever. Not "went stale and
recovered late" — for ever: the tab claimed CONNECTED, the elapsed timer went
on counting, and the only thing that ever corrected it was closing the
application. That is the defect this fixes, and it turned out to be two
separate failures wearing one symptom.
◆ THE HANG IS IN SSH.NET, AND KEEP-ALIVE ALONE WOULD NOT HAVE FIXED IT.
ShellStream ends a blocked read from exactly two places: its channel being
closed, and the session raising Disconnected. A shell that exits gives the
first. But Session raises Disconnected from one method only — OnDisconnectReceived,
for a server that politely says goodbye — and a socket that simply fails goes
down a different path entirely: RaiseError fires the error event, sets the
disconnecting flag and disposes the socket, without ever raising Disconnected.
So the stream was never disposed, its Monitor.Wait was never pulsed, and the
read blocked until the process ended. The transport was gone, the client knew
the transport was gone, and nothing woke the one thread that was waiting.
That is why the first attempt at a test for this sat at thirty seconds and
failed. Both halves are needed and neither is sufficient:
- Keep-alive makes the failure HAPPEN. An idle SSH session is a socket
nobody writes to, and TCP has no way to discover that the far end has gone
without writing to it — nothing was asked, so nothing could fail. Thirty
seconds, matching OpenSSH's own ServerAliveInterval examples. It is the
write that matters and not a reply: SSH_MSG_IGNORE need not be answered,
so this is traffic rather than a heartbeat with a deadline, and there is
no timeout invented here to be wrong about a slow link.
- Disposing the stream from the error handler makes it ARRIVE. Queued to the
thread pool and not done inline, which is not tidiness: this runs on
SSH.NET's message-listener thread, and closing a channel waits for the
server's close — processed by that same thread — so an inline dispose is
the listener thread waiting on itself.
SFTP gets the keep-alive too. Its operations are request/response and bounded
by ConnectionInfo.Timeout already, so it needs no equivalent wake-up.
◆ exit AND reboot ARE TOLD APART BY A LATCH, BECAUSE THE OBVIOUS WAY IS A RACE.
Wait for the read to end and then ask whether the transport is still up, and
against a real server the answer is wrong about half the time: this was the
only channel on the connection, so sshd hangs up a millisecond after closing
it. By the time anything could ask, the transport is gone either way — and a
poll would report every ordinary `exit` as a dropped connection.
So ISshShellSession.TransportFailure is set at the moment SSH.NET raises the
failure, which its own ordering puts strictly before the disconnect that wakes
the read. A read that has just returned zero consults a settled value: set if
the transport failed, still null if the remote merely closed the channel. The
pump turns that into a TerminalSessionOutcome — RemoteClosed, ConnectionLost,
ClosedLocally — carried on SessionEnded, which used to hand over a session id
and nothing else. A subscriber cannot work this out afterwards; by the time it
hears, there is no transport left to interrogate.
Deliberately no fourth value for "the remote rebooted". Nothing on the wire
distinguishes a reboot from a cable being pulled, and a name this client
cannot check would be a guess dressed as a fact.
◆ WHAT IS DRAWN, AND WHY IT IS NOT DRAWN OVER THE PANE.
The status bar says DISCONNECTED or SHELL EXITED where it used to say NOT
CONNECTED for both — a word that is honest about a tab still dialling and
useless about one whose shell has stopped. Red for the first and grey for the
second: one is a machine that stopped answering and the other is work
finished, and crying wolf on the ordinary end of every session is its own bug.
The tab strip's dot gains the same fourth state, on both heads.
Under the terminal, a strip carrying the reason and two buttons. It takes its
own Auto grid row rather than overlaying the renderer, and that is forced
rather than chosen: the WebView is a native child view composited above
everything Avalonia draws, so the alternative is collapsing the terminal — and
the scrollback is precisely what the user wants to read, because the last
thing the remote said is usually why it went. terminal.js already writes the
same sentence into the pane; this is the copy that does not scroll away.
RECONNECT reuses the tab in place, which is what the word means to somebody
who pressed it: the terminal they were working in comes back, where it was.
It dials through the vault's ordinary connect path, so every refusal — a key
that has left the keychain, an unknown host key, a group default that has
changed since — is decided in the one place that already decides them; what
this adds is which tab the attempt belongs to. The old session is closed as
part of it, because a dead shell still holds an authenticated connection this
process has stopped reading, and its pane still holds an xterm, a scrollback
and a WebGL context, of which a browser hands out about sixteen.
Two refusals it makes rather than pretending: a host reached by typing an
address has no keychain entry to redial, and one that wants a password typed
goes to the keychain with the sheet raised rather than being dialled with an
empty box. Both say so on the strip where the button was pressed, not only in
the vault's status on a screen that surface is not.
TRY AGAIN appears on a refused connection for the same reason, and it is worth
as much: the commonest refusal is a host that was not up yet.
◆ WHAT IS ACTUALLY VERIFIED.
Two tests against a real sshd in a container. `exit` is reported as the remote
closing the session — the one a polling implementation fails, and a fake shell
cannot show it because a fake has no server behind it deciding to hang up. And
a live shell losing its transport is reported as a lost connection, produced
by dialling through a plain TCP relay the test owns and then cutting it; the
container is shared by the collection, so killing sshd was never an option.
The first draft of that second test used LoopbackProxyTests' SOCKS forward and
timed out: disconnecting the carrier of a ForwardedPortDynamic does not close
the loopback socket it accepted. Recorded in the test, because it is a
property of SSH.NET's forwarder rather than of anything here, and it is not
what a dying machine looks like.
What no test reproduces is the waiting. A relay that closes sends a FIN, so
the socket reports itself at once; a machine switched off sends nothing, and
finding that out is the keep-alive's business and the OS's retransmit budget's
— neither of which a test can hurry along without a clock this code does not
own. A reboot is prompt in practice because the machine comes back and RSTs.
Beside those: three pump outcomes, three workspace announcements, and seven
shell-flow tests covering the drop, the clean exit, reuse of the tab, retry
after a refusal, and both ways a reconnect can honestly come to nothing.
Also fixed in passing: SshNetShellSession.IsOpen read ShellStream.CanRead,
which is documented as always true — the stream stays readable after disposal
so buffered output can be drained — so it had answered "yes" for every shell
that ever existed. It reads CanWrite now. And MainWindowViewModel's session-
ended handler moved from Dispatcher.UIThread.Post to the injected post
delegate, which is what makes any of the shell half testable at all; the
remark listing the three unreachable posts is one shorter.
554 lines
31 KiB
XML
554 lines
31 KiB
XML
<Styles xmlns="https://github.com/avaloniaui"
|
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
|
|
|
<!--
|
|
The phone's own control styles. The desktop head has an equivalent block in its App.axaml and the two
|
|
deliberately do not share one: the palette is shared because a colour must not drift, but a control's
|
|
size is a property of the input device. Every touch target here is at least 44 effective pixels tall,
|
|
which is the number the desktop's 28-pixel rows would fail.
|
|
|
|
Classes rather than implicit styles for the buttons, because the shell needs three that differ in
|
|
meaning rather than in degree: the accent one is what the screen wants you to do, the outlined one is
|
|
the alternative, and the red one is the one that costs something.
|
|
|
|
── v2 ──────────────────────────────────────────────────────────────────────────────────────────────
|
|
The second design rounded everything on its own ladder — 4 for a tag, 9 for a button or a pill, 10 for
|
|
a list row, 11 for the search well, 12 for a card, 14 for a block of monospaced output — and the numbers
|
|
below carried it for three passes: the radius said how big the thing was, so a 12 on a chip or a 4 on a
|
|
card read as the wrong size before it read as the wrong shape. Left as a record of that reasoning rather
|
|
than deleted, because the reasoning still holds; only the numbers it was reasoning about have moved.
|
|
|
|
── v5 ──────────────────────────────────────────────────────────────────────────────────────────────
|
|
This pass takes the desktop's v5 ladder instead of v2's own, on the same reversal recorded for the
|
|
fonts: the phone now matches the desktop's shape as well as its face. Three rungs rather than six —
|
|
cards and sections stay 12, a button or a field is 10, a chip or a tag is 6 — collapsing v2's 9/10/11
|
|
into the one value the desktop's buttons and fields already use, and moving its 4 up to 6 and its 14
|
|
down to 12 to land on the desktop's own chip and card numbers. The ladder still says how big a thing
|
|
is before it says what shape it is; it is just a shorter ladder now, because the desktop it is copying
|
|
never drew v2's 11-radius search well or a card any rounder than a chip's neighbour a step away, and the
|
|
same case that closed the gap between 9, 10 and 11 closes the one between 12 and 14.
|
|
|
|
Not every radius on this head belongs to this ladder. The floating action button is 28 — half its own
|
|
56, which is a circle rather than a ladder rung — and the sheets stay at 22 on their top corners only,
|
|
which is a phone idiom this codebase's own bottom sheets have used since v2 and the desktop draws
|
|
nothing like. Both are documented where they are set rather than here, for the reason FAB and sheet
|
|
radii are always documented locally: a reader who only ever meets one of them should not have to find
|
|
this paragraph to learn it was deliberate.
|
|
-->
|
|
|
|
<Style Selector="Button.primary">
|
|
<Setter Property="Height" Value="48" />
|
|
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
|
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
|
<Setter Property="Foreground" Value="{StaticResource AccentInk}" />
|
|
<Setter Property="CornerRadius" Value="10" />
|
|
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
|
<Setter Property="FontSize" Value="12" />
|
|
<Setter Property="FontWeight" Value="SemiBold" />
|
|
</Style>
|
|
|
|
<!--
|
|
◆ Gradient and glow, replacing a flat Accent fill — the same swap App.axaml's Button.accent made on the
|
|
desktop, and for the same reason: this design draws every primary button top-to-bottom from a lighter
|
|
violet into the accent proper, lifted off the surface with a soft violet glow rather than a border. Both
|
|
live in Palette.axaml as AccentGradient and AccentGlow, already resolvable here because the palette is
|
|
shared — this is a resource-key swap, not new colour.
|
|
|
|
Background and BoxShadow are set here rather than as plain Setters above, because Fluent's default
|
|
button template only lets a style reach the fill and the glow through the ContentPresenter it draws
|
|
itself around — the same reason the desktop's rule targets /template/ ContentPresenter rather than the
|
|
Button. Height, radius and the rest stay ordinary Setters on Button.primary itself; only the two the
|
|
template intercepts move down here.
|
|
-->
|
|
<Style Selector="Button.primary /template/ ContentPresenter">
|
|
<Setter Property="Background" Value="{StaticResource AccentGradient}" />
|
|
<Setter Property="BoxShadow" Value="{StaticResource AccentGlow}" />
|
|
</Style>
|
|
<Style Selector="Button.primary:pressed /template/ ContentPresenter">
|
|
<Setter Property="Background" Value="{StaticResource AccentGradient}" />
|
|
<Setter Property="Opacity" Value="0.82" />
|
|
</Style>
|
|
|
|
<!--
|
|
Disabled is drawn as flat and unlit rather than merely dimmed. The design's CONTINUE button on the
|
|
recovery screen is disabled until the checkbox is ticked, and a user who cannot tell it is disabled
|
|
reads the screen as broken rather than as waiting for them.
|
|
|
|
BoxShadow has to be cleared here too, as "none" rather than left unset — see the remark on
|
|
Button.accent:disabled in the desktop's App.axaml for why the literal string is required and an empty
|
|
BoxShadows is not: the base rule's glow Setter is still in effect wherever a more specific one does not
|
|
override it, and a disabled primary button lit with a glow would read as wanting to be pressed.
|
|
-->
|
|
<Style Selector="Button.primary:disabled /template/ ContentPresenter">
|
|
<Setter Property="Background" Value="{StaticResource Raised}" />
|
|
<Setter Property="BoxShadow" Value="none" />
|
|
<Setter Property="TextElement.Foreground" Value="{StaticResource TextFaint}" />
|
|
</Style>
|
|
|
|
<Style Selector="Button.secondary">
|
|
<Setter Property="Height" Value="48" />
|
|
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
|
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
|
<Setter Property="Background" Value="Transparent" />
|
|
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
|
|
<Setter Property="BorderThickness" Value="1" />
|
|
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
|
<Setter Property="CornerRadius" Value="10" />
|
|
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
|
<Setter Property="FontSize" Value="11.5" />
|
|
<Setter Property="FontWeight" Value="SemiBold" />
|
|
</Style>
|
|
|
|
<Style Selector="Button.danger">
|
|
<Setter Property="Height" Value="48" />
|
|
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
|
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
|
<Setter Property="Background" Value="Transparent" />
|
|
<Setter Property="BorderBrush" Value="{StaticResource DangerSoft}" />
|
|
<Setter Property="BorderThickness" Value="1" />
|
|
<Setter Property="Foreground" Value="{StaticResource Danger}" />
|
|
<Setter Property="CornerRadius" Value="10" />
|
|
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
|
<Setter Property="FontSize" Value="10.5" />
|
|
<Setter Property="FontWeight" Value="SemiBold" />
|
|
</Style>
|
|
|
|
<!--
|
|
A row in a list: the whole row is the target, and it is 54 tall because a thumb is not a mouse.
|
|
|
|
◆ VerticalContentAlignment, because that height is the whole point of this class and Avalonia's default
|
|
for content alignment is Stretch — so the content presenter stretched the caption to the full row and a
|
|
TextBlock draws its line at the TOP of what it is given. Most rows here never showed it, having a
|
|
StackPanel or a Grid of already-centred children in them, which is what made the four that did look
|
|
like four unrelated mistakes: the breadcrumb chips and the up-one-directory button on FilesScreen, and
|
|
TerminalScreen's CLOSE THIS TAB, each a bare TextBlock in a row 36 or 44 tall with no vertical padding.
|
|
Measured at those numbers, the caption sat flush against the top edge with 21 to 33 pixels below it.
|
|
|
|
The desktop head's App.axaml carries the same setter on its own shapes for the same reason, and excludes
|
|
two of them — see the remark on Button.ghost there. Nothing is excluded here: no row's content depends
|
|
on being stretched, there being no full-height strip inside any of the thirty-three, and the Grids that
|
|
stop filling hold only children that already centre themselves, so they land where they always did.
|
|
-->
|
|
<Style Selector="Button.row">
|
|
<Setter Property="MinHeight" Value="54" />
|
|
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
|
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
|
<Setter Property="Background" Value="Transparent" />
|
|
<Setter Property="BorderThickness" Value="0" />
|
|
<Setter Property="CornerRadius" Value="10" />
|
|
<Setter Property="Padding" Value="16,6" />
|
|
</Style>
|
|
<Style Selector="Button.row:pressed /template/ ContentPresenter">
|
|
<Setter Property="Background" Value="{StaticResource Hover}" />
|
|
</Style>
|
|
|
|
<!--
|
|
The 44-pixel square in a header — a back arrow, an add, an upload. Square rather than stretched because
|
|
it sits beside a title that must keep the width it has, and 44 rather than the glyph's own size because
|
|
a 17-pixel arrow is a 17-pixel target.
|
|
-->
|
|
<Style Selector="Button.icon">
|
|
<Setter Property="Width" Value="44" />
|
|
<Setter Property="Height" Value="44" />
|
|
<Setter Property="Padding" Value="0" />
|
|
<Setter Property="Background" Value="Transparent" />
|
|
<Setter Property="BorderThickness" Value="0" />
|
|
<Setter Property="CornerRadius" Value="10" />
|
|
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
|
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
|
<Setter Property="FontSize" Value="17" />
|
|
</Style>
|
|
<Style Selector="Button.icon.accent">
|
|
<Setter Property="Foreground" Value="{StaticResource AccentText}" />
|
|
<Setter Property="FontSize" Value="20" />
|
|
</Style>
|
|
<Style Selector="Button.icon:pressed /template/ ContentPresenter">
|
|
<Setter Property="Background" Value="{StaticResource ChromeHover}" />
|
|
</Style>
|
|
|
|
<!--
|
|
The floating action button the design has asked for since v2, and which this theme refused twice — on
|
|
the grounds that it would open an editor this head had not got, and that an unused style is a claim the
|
|
control exists somewhere. Both were true. The second is why the style arrives in the same commit as the
|
|
button rather than ahead of it.
|
|
|
|
56 rather than the 44 the header icons use. It is the only control on the screen that floats over
|
|
content rather than sitting in a bar, so it has no neighbouring chrome to borrow an edge from, and a
|
|
thumb reaching the bottom-right corner of a 360dp screen is doing it without looking.
|
|
|
|
Accent-filled, which it shares with Button.primary and with nothing else — and it means the same thing
|
|
in both places: the one action on the surface that is not a choice between peers. Circular by radius
|
|
rather than by a Path, so the pressed state the template draws is the same shape as the button — 28,
|
|
exactly half its own 56, which is a geometric constraint rather than a ladder rung and does not move
|
|
with the rest of this file's radii.
|
|
|
|
◆ Gradient and glow since v5, matching Button.primary rather than staying a flat fill once that one
|
|
moved. The two are the only accent-filled controls on this head and are read as one idea — "the thing
|
|
this surface wants you to do" — so a flat FAB beside a gradient primary button would be the seam this
|
|
codebase's palette file keeps warning about, one screenshot over from the button it echoes. The glow
|
|
reads as well on a floating circle as it does on a bar-anchored rectangle: if anything a control that
|
|
already floats over content earns a lift more than one sitting in a row of chrome does, so it is kept
|
|
rather than dropped for the FAB's own shape.
|
|
|
|
Still only on HOSTS. The design puts a second one on S3 and that editor genuinely does not exist yet, so
|
|
the style being here is not permission to draw one there.
|
|
-->
|
|
<Style Selector="Button.fab">
|
|
<Setter Property="Width" Value="56" />
|
|
<Setter Property="Height" Value="56" />
|
|
<Setter Property="Padding" Value="0" />
|
|
<Setter Property="BorderThickness" Value="0" />
|
|
<Setter Property="CornerRadius" Value="28" />
|
|
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
|
<Setter Property="Foreground" Value="{StaticResource AccentInk}" />
|
|
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
|
<Setter Property="FontSize" Value="24" />
|
|
<Setter Property="FontWeight" Value="SemiBold" />
|
|
</Style>
|
|
<!-- See the remark above Button.primary's own /template/ ContentPresenter rule: BoxShadow has no home on
|
|
a Button itself, so the fill moves down here alongside it rather than staying a plain Setter. -->
|
|
<Style Selector="Button.fab /template/ ContentPresenter">
|
|
<Setter Property="Background" Value="{StaticResource AccentGradient}" />
|
|
<Setter Property="BoxShadow" Value="{StaticResource AccentGlow}" />
|
|
</Style>
|
|
<Style Selector="Button.fab:pressed /template/ ContentPresenter">
|
|
<Setter Property="Background" Value="{StaticResource AccentGradient}" />
|
|
<Setter Property="Opacity" Value="0.82" />
|
|
</Style>
|
|
|
|
<!--
|
|
The dimmed area behind a sheet, and a Button rather than a Border because tapping it has to mean
|
|
something — see the add sheet in HostsScreen, which is dismissible for the reason the host-key sheet is
|
|
not.
|
|
|
|
62% of Canvas, written out because the palette holds no alpha variant of a surface. The pressed rule is
|
|
not decoration: a Button's default template lights its ContentPresenter when touched, so without it the
|
|
scrim would flash a pale rectangle across the whole screen on the way to closing — which reads as the
|
|
interface glitching rather than as a tap being received.
|
|
-->
|
|
<Style Selector="Button.scrim">
|
|
<Setter Property="Background" Value="#9E05050A" />
|
|
<Setter Property="BorderThickness" Value="0" />
|
|
<Setter Property="CornerRadius" Value="0" />
|
|
<Setter Property="Padding" Value="0" />
|
|
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
|
<Setter Property="VerticalAlignment" Value="Stretch" />
|
|
</Style>
|
|
<Style Selector="Button.scrim:pointerover /template/ ContentPresenter">
|
|
<Setter Property="Background" Value="#9E05050A" />
|
|
</Style>
|
|
<Style Selector="Button.scrim:pressed /template/ ContentPresenter">
|
|
<Setter Property="Background" Value="#9E05050A" />
|
|
</Style>
|
|
|
|
<!--
|
|
A card: one item, one rule, one thing you can act on. The design uses it for a key, a snippet, a
|
|
transfer and the panel at the foot of the keychain, and they are all the same shape deliberately —
|
|
the screens differ in what they hold rather than in how they are built.
|
|
-->
|
|
<Style Selector="Border.card">
|
|
<Setter Property="Background" Value="{StaticResource Sidebar}" />
|
|
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
|
|
<Setter Property="BorderThickness" Value="1" />
|
|
<Setter Property="CornerRadius" Value="12" />
|
|
<Setter Property="Padding" Value="14,13" />
|
|
</Style>
|
|
|
|
<!--
|
|
A block of monospaced output — a snippet's command, a log, the terminal. One surface for all three, and
|
|
it is not one of the palette's chrome greys: see TerminalSurface in Palette.axaml.
|
|
-->
|
|
<Style Selector="Border.output">
|
|
<Setter Property="Background" Value="{StaticResource TerminalSurface}" />
|
|
<Setter Property="CornerRadius" Value="12" />
|
|
<Setter Property="Padding" Value="12" />
|
|
</Style>
|
|
|
|
<!--
|
|
A small monospaced tag: a vault's name, a sync badge, what is open on the files screen. Not a control —
|
|
it is never pressed — so a Border rather than a Button, and the class exists so that the four screens
|
|
drawing one cannot disagree about its padding.
|
|
-->
|
|
<Style Selector="Border.tag">
|
|
<Setter Property="Background" Value="{StaticResource Chip}" />
|
|
<Setter Property="CornerRadius" Value="6" />
|
|
<Setter Property="Padding" Value="7,2" />
|
|
<Setter Property="VerticalAlignment" Value="Center" />
|
|
</Style>
|
|
<Style Selector="Border.tag > TextBlock">
|
|
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
|
<Setter Property="FontSize" Value="9" />
|
|
<Setter Property="FontWeight" Value="Medium" />
|
|
<Setter Property="Foreground" Value="{StaticResource AccentText}" />
|
|
</Style>
|
|
|
|
<!--
|
|
A tag that is also a button: the picker in the host editor. It borrows Border.tag's geometry rather than
|
|
inheriting it, because a Button is not a Border and Avalonia's selectors are structural — the numbers
|
|
are repeated and the two have to move together, which is why they sit adjacent.
|
|
|
|
Worn is filled, unworn is outlined, so a row of these reads as the host's tags rather than as a form
|
|
about them. 36 tall rather than 44: it is a chip, its neighbours are chips, and a row of 44-pixel pills
|
|
would be a wall. The whole chip is the target and they are spaced 6 apart, which is what keeps a miss
|
|
landing on nothing rather than on the wrong tag.
|
|
-->
|
|
<Style Selector="Button.chiptoggle">
|
|
<Setter Property="MinHeight" Value="36" />
|
|
<Setter Property="Background" Value="Transparent" />
|
|
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
|
|
<Setter Property="BorderThickness" Value="1" />
|
|
<Setter Property="CornerRadius" Value="6" />
|
|
<Setter Property="Padding" Value="11,0" />
|
|
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
|
</Style>
|
|
<Style Selector="Button.chiptoggle.worn">
|
|
<Setter Property="BorderBrush" Value="Transparent" />
|
|
<Setter Property="Foreground" Value="{StaticResource AccentText}" />
|
|
</Style>
|
|
<Style Selector="Button.chiptoggle.worn /template/ ContentPresenter">
|
|
<Setter Property="Background" Value="{StaticResource Chip}" />
|
|
</Style>
|
|
<Style Selector="Button.chiptoggle:pressed /template/ ContentPresenter">
|
|
<Setter Property="Background" Value="{StaticResource Hover}" />
|
|
</Style>
|
|
|
|
<!-- An outlined tag, for a fact about an item rather than a name: a type, an algorithm, a count. -->
|
|
<Style Selector="Border.tag.outline">
|
|
<Setter Property="Background" Value="Transparent" />
|
|
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
|
|
<Setter Property="BorderThickness" Value="1" />
|
|
<Setter Property="Padding" Value="5,1" />
|
|
</Style>
|
|
<Style Selector="Border.tag.outline > TextBlock">
|
|
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
|
<Setter Property="FontSize" Value="8.5" />
|
|
</Style>
|
|
|
|
<!--
|
|
A category chip. A RadioButton rather than a ToggleButton because the options are exclusive and a screen
|
|
reader should say so; the bullet its default template draws is removed, since the chip's own fill is
|
|
what carries the state.
|
|
|
|
Checked is a filled surface with accent *text*, not an accent fill. v2 makes that distinction
|
|
everywhere — see the remark on AccentText in Palette.axaml — and a chip is where it matters most: a row
|
|
of four solid blue lozenges is a row of four things that all look like the primary action.
|
|
|
|
◆ Its own radius rather than Border.tag's or Button.chiptoggle's, despite the name. v2 drew this control
|
|
on the ladder's button-or-pill rung rather than its tag rung — 9, not 4 — and the v5 pass carries the
|
|
same rung forward to 10 rather than to Border.tag's 6: it is a much larger control at 34 tall against a
|
|
tag's line-height, and a radius picked for a 34-pixel chip is not the one a 20-pixel tag needs, whatever
|
|
the class is called. See the remark on Phone.axaml's own ladder, above.
|
|
-->
|
|
<Style Selector="RadioButton.chip">
|
|
<Setter Property="MinHeight" Value="34" />
|
|
<Setter Property="Padding" Value="13,6" />
|
|
<Setter Property="CornerRadius" Value="10" />
|
|
<Setter Property="Background" Value="Transparent" />
|
|
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
|
|
<Setter Property="BorderThickness" Value="1" />
|
|
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
|
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
|
<Setter Property="FontSize" Value="10.5" />
|
|
<Setter Property="FontWeight" Value="Medium" />
|
|
<Setter Property="Template">
|
|
<ControlTemplate>
|
|
<Border Name="Chip" Background="{TemplateBinding Background}"
|
|
BorderBrush="{TemplateBinding BorderBrush}"
|
|
BorderThickness="{TemplateBinding BorderThickness}"
|
|
CornerRadius="{TemplateBinding CornerRadius}"
|
|
Padding="{TemplateBinding Padding}"
|
|
MinHeight="{TemplateBinding MinHeight}">
|
|
<ContentPresenter Content="{TemplateBinding Content}"
|
|
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
|
</Border>
|
|
</ControlTemplate>
|
|
</Setter>
|
|
</Style>
|
|
<Style Selector="RadioButton.chip:checked">
|
|
<Setter Property="Background" Value="{StaticResource Active}" />
|
|
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
|
|
<Setter Property="Foreground" Value="{StaticResource AccentText}" />
|
|
<Setter Property="FontWeight" Value="SemiBold" />
|
|
</Style>
|
|
|
|
<!--
|
|
No segmented control either. The design draws one over the file listing for LOCAL / REMOTE, and there is
|
|
no local side on a phone — see FilesScreen. The two-way choices that are real, the log's two logs and
|
|
the keychain's categories, are chip rows, which is the same control at the same size without a well
|
|
around it.
|
|
-->
|
|
|
|
<Style Selector="TextBox.field">
|
|
<Setter Property="Height" Value="44" />
|
|
<Setter Property="Padding" Value="14,0" />
|
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
|
<Setter Property="Background" Value="{StaticResource Field}" />
|
|
<Setter Property="BorderBrush" Value="{StaticResource BorderMid}" />
|
|
<Setter Property="BorderThickness" Value="1" />
|
|
<Setter Property="CornerRadius" Value="10" />
|
|
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
|
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
|
<Setter Property="FontSize" Value="12" />
|
|
</Style>
|
|
|
|
<!--
|
|
Every box on this head that takes a secret, and it is a class rather than two attributes repeated five
|
|
times because the two attributes are not interchangeable and both are needed.
|
|
|
|
<b>PasswordChar is what the screen shows; ContentType is what the keyboard is told.</b> The desktop head
|
|
needs only the first — a Windows text box has no opinion about what is being typed into it. Android's
|
|
software keyboard does: left at its default it treats a passphrase box as ordinary prose, which means
|
|
the suggestion strip offers completions from everything that phone has ever been typed into, and the
|
|
IME's own learning dictionary remembers what was typed. Dots on screen and a word-suggestion bar above
|
|
them is the worst of both — the secret is hidden from the person typing it and offered to the room.
|
|
TextInputOptions.ContentType is the property the Android backend maps onto InputType, and Password is
|
|
what turns the suggestions off and keeps the entry out of the dictionary.
|
|
|
|
A field class is not implied. Two of the five boxes carry their own metrics — see LockedScreen — so this
|
|
one sets nothing about size or colour, and the other three say Classes="field secret".
|
|
-->
|
|
<Style Selector="TextBox.secret">
|
|
<Setter Property="PasswordChar" Value="•" />
|
|
<Setter Property="TextInputOptions.ContentType" Value="Password" />
|
|
</Style>
|
|
|
|
<!--
|
|
The live dot, and the class name is the same one the desktop sidebar uses so the two heads cannot
|
|
drift on what green means: a terminal is open on this host right now. Deliberately not reachability —
|
|
nothing pings anything, and a dot meaning "up" would be a claim the application never checks.
|
|
|
|
Live rather than Accent since v2. The accent is blue now and this is not an accent: it is a fact about
|
|
a host. See the remark on Live in Palette.axaml.
|
|
-->
|
|
<Style Selector="Ellipse.dot">
|
|
<Setter Property="Width" Value="7" />
|
|
<Setter Property="Height" Value="7" />
|
|
<Setter Property="Fill" Value="{StaticResource TextGhost}" />
|
|
</Style>
|
|
<Style Selector="Ellipse.dot.live">
|
|
<Setter Property="Fill" Value="{StaticResource Live}" />
|
|
</Style>
|
|
|
|
<!--
|
|
Amber, and it does not contradict the remark above. That one says green is a fact about a host rather
|
|
than an accent, and this is the colour for a fact that is not settled yet: green is what is true, purple
|
|
is what you can press, and a connection still being made is neither. The palette's own rule gives amber
|
|
to the caveat worth reading, which is exactly what this is.
|
|
|
|
Only the tab strips use it, and only for a tab with no shell behind it yet — the same amber, from the
|
|
same brush, as the track and the running step on the connecting screen, so that a tab and the screen it
|
|
opens agree about what is happening. See TerminalScreen.axaml.
|
|
-->
|
|
<Style Selector="Ellipse.dot.connecting">
|
|
<Setter Property="Fill" Value="{StaticResource Warn}" />
|
|
</Style>
|
|
|
|
<!--
|
|
Red for a shell whose connection broke, splitting the grey that used to cover every dead tab. A shell
|
|
somebody exited and a shell that was cut off are the two dead states with the least in common, and on a
|
|
phone the difference matters more than it does on a desk: a mobile link drops on its own, and the tab
|
|
that went quiet while the screen was off is the one worth dialling again. See
|
|
TerminalTabViewModel.IsDropped.
|
|
-->
|
|
<Style Selector="Ellipse.dot.dropped">
|
|
<Setter Property="Fill" Value="{StaticResource Danger}" />
|
|
</Style>
|
|
|
|
<!-- Every label, count, address and fingerprint in this design is monospace. See Palette.axaml. -->
|
|
<Style Selector="TextBlock.mono">
|
|
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
|
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
|
<Setter Property="FontSize" Value="12" />
|
|
</Style>
|
|
<Style Selector="TextBlock.label">
|
|
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
|
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
|
<Setter Property="FontSize" Value="9.5" />
|
|
<Setter Property="FontWeight" Value="SemiBold" />
|
|
</Style>
|
|
<Style Selector="TextBlock.detail">
|
|
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
|
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
|
|
<Setter Property="FontSize" Value="10" />
|
|
</Style>
|
|
<Style Selector="TextBlock.body">
|
|
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
|
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
|
<Setter Property="FontSize" Value="11" />
|
|
<Setter Property="LineHeight" Value="19" />
|
|
<Setter Property="TextWrapping" Value="Wrap" />
|
|
</Style>
|
|
<Style Selector="TextBlock.title">
|
|
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
|
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
|
<Setter Property="FontSize" Value="15" />
|
|
<Setter Property="FontWeight" Value="SemiBold" />
|
|
</Style>
|
|
|
|
<!--
|
|
A screen's own name, in the header. The one place this design is not monospaced: the design sets every
|
|
heading in its sans face and keeps the mono for what is machine-shaped — an address, a fingerprint, a
|
|
count, a command. That split is worth honouring even though both resolve to substituted fonts here.
|
|
-->
|
|
<Style Selector="TextBlock.heading">
|
|
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
|
<Setter Property="FontSize" Value="17" />
|
|
<Setter Property="FontWeight" Value="SemiBold" />
|
|
<Setter Property="VerticalAlignment" Value="Center" />
|
|
</Style>
|
|
|
|
<!--
|
|
A group heading over a list: PRODUCTION, STAGING. Letter-spaced, and quieter than the rows under it —
|
|
but TextDim rather than the design's own #565E7E. In a flat list these headings are the only thing
|
|
separating one block of hosts from the next, and at 10px with a letter-space the fainter step is under
|
|
3:1 on this background. Quieter than a host row, still readable, which is what "quieter" has to mean.
|
|
-->
|
|
<Style Selector="TextBlock.section">
|
|
<Setter Property="Foreground" Value="{StaticResource TextDim}" />
|
|
<Setter Property="FontSize" Value="10" />
|
|
<Setter Property="FontWeight" Value="SemiBold" />
|
|
<Setter Property="LetterSpacing" Value="1" />
|
|
</Style>
|
|
|
|
<!--
|
|
A fingerprint, and it is a style rather than four repeated setters because the rule it encodes is a
|
|
product rule: never truncated, and always breakable across lines rather than clipped. Four screens show
|
|
one, and the day one of them clips is the day somebody approves the wrong key.
|
|
-->
|
|
<!--
|
|
◆ Declared twice, and this is the bug the rule above exists to prevent rather than a tidiness problem.
|
|
|
|
Avalonia's type selectors are exact: `TextBlock.fingerprint` does not match a SelectableTextBlock, which
|
|
derives from it. Every fingerprint on this head is selectable — you want to be able to copy one and
|
|
compare it — so with only the first selector the style applied to none of them, and a SHA256 fingerprint
|
|
rendered in the proportional font at the inherited size with TextWrapping left at NoWrap. At 360dp that
|
|
runs off the edge and clips, which is exactly the thing the comment above says must never happen.
|
|
|
|
The desktop head already carries the same pair for `mono` (App.axaml), for the same reason. Avalonia has
|
|
an `:is(TextBlock)` form that would match both in one selector; two explicit lines are used here because
|
|
the duplication is the thing worth seeing when somebody adds a third fingerprint site.
|
|
-->
|
|
<Style Selector="TextBlock.fingerprint">
|
|
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
|
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
|
<Setter Property="FontSize" Value="11" />
|
|
<Setter Property="LineHeight" Value="18" />
|
|
<Setter Property="TextWrapping" Value="Wrap" />
|
|
</Style>
|
|
<Style Selector="SelectableTextBlock.fingerprint">
|
|
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
|
|
<Setter Property="Foreground" Value="{StaticResource Text}" />
|
|
<Setter Property="FontSize" Value="11" />
|
|
<Setter Property="LineHeight" Value="18" />
|
|
<Setter Property="TextWrapping" Value="Wrap" />
|
|
</Style>
|
|
|
|
</Styles>
|