Files
DodoSSH/src/DodoSSH.Client.Android/Views/LogsScreen.axaml
T
jaap-jan aff776cb42
ci / build and test (push) Failing after 3m0s
ci / desktop nightly (push) Skipped
ci / api image (push) Skipped
ci / android head (push) Successful in 3m56s
Rebuild the terminal's keypad, and fix what a device run showed
Six defects, all found by running this head on an emulator against a local API rather than by reading it.
They are in one commit because two of the files carry several of them and splitting those would mean
hand-editing patches; each is argued at its own site.

── the keypad, which is most of this ───────────────────────────────────────────────────────────────────
It was eleven 30-by-44 keys in a horizontal ScrollViewer, and it was wrong twice over. A pan and a press
begin with the same few pixels of travel, so a thumb coming down at an angle — which is how a thumb comes
down — was read as the start of a pan and the press was never delivered; the row slid slightly instead.
And 30 tall is well under the 48 Android asks for, so the presses that did arrive were hard to aim. The
arrows, the keys pressed most and in sequence, were the ones parked past the right-hand edge.

Termux, Termius, JuiceSSH, ConnectBot and Blink all ship this row and not one of them scrolls it: they
divide the width evenly and add a second row when the set does not fit. That is not convention for its own
sake — a key whose position is fixed can be hit without looking, which is the whole point of a modifier row
used while reading the output above it.

So: two UniformGrid rows, every key an equal share of the width, nothing scrollable, 40 tall. Every key
kept. Arrows on the lower row where the thumb rests, reordered to ← ↓ ↑ → so the pairs that undo each other
are neighbours. It costs about 51 pixels of terminal, four or five lines, and that is the right trade: the
row that was cheaper did not work.

The keys also gain a pressed state, which they had none of. They are Focusable="False" so that using one
does not take the keyboard off the terminal, and a button that never takes focus never shows the adorner
Fluent would otherwise give it — leaving no feedback at all on a control whose only job is to be pressed.
Their looks move from assignments in the code-behind to Button.hotkey, which is what lets a style say
:pressed, and the Ctrl latch becomes a class rather than two brushes written by hand.

── and five smaller ones ────────────────────────────────────────────────────────────────────────────────
The keyboard's inset was never cleared when the app was resumed without the keyboard it was applied for.
Android takes the keyboard away as the activity stops and Avalonia's input pane raises no StateChanged,
the pane going with the window rather than closing in front of one — so the interface came back holding a
whole keyboard's height of empty canvas. 838 of 2340 pixels on a Pixel 5, on every screen, until something
opened and closed the keyboard. MainActivity now raises PhoneEnvironment.Resumed and PhoneShell re-asks the
pane. It asks rather than assuming zero: a permission prompt or the biometric sheet can hand the activity
back with the keyboard genuinely still up, and flattening the inset there would strand the focused box
under a real one.

TextBlock.detail never wrapped, where .body beside it always has. A block that neither wraps nor trims lays
out at its full single-line width and the parent clips it, with no ellipsis to say anything is missing — so
the add sheet read "A machine to connect to, filed under a group if you" and stopped. Which of the fifty-odd
.detail blocks overflows is a function of width and font scale rather than of the string, so this is a style
rather than an attribute on the handful visibly wrong on one phone. The nine that genuinely want one line
now say NoWrap beside their TextTrimming, which is the pairing that was always meant.

The two port boxes are NumericUpDowns and were the only unthemed controls on this head — Fluent's border and
fill, next to a TextBox.field sharing their row. App.axaml has styled NumericUpDown for the desktop all
along; this head never had one.

Three buttons classed "icon accent" were painting Fluent's own Button.accent, #6275AC, a blue in no palette
here. The desktop overrides that style and this head did not, so the same class read correctly there and not
here.

The host key sheet said "First contact with 10.0.2.2 · 2222 . Nothing in this keychain…" — Avalonia puts a
space between adjacent Runs and the last one began with the full stop. A gap before a full stop, on the one
dialogue whose whole job is to be read carefully before it is answered.

── checks ──────────────────────────────────────────────────────────────────────────────────────────────
manual-checks gains 10.4a and 11.10f. 10.4 already covered dismissing the keyboard with back or the
chevron but not leaving the app with it up, which is the path the inset bug shipped through; 10.4a covers
both directions, since a fix that flattened the inset on every resume would pass one half and fail the
other. 11.10f guards the keypad against a ScrollViewer being put back around it.
2026-08-26 13:28:59 +02:00

180 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.Android.Views"
x:Class="DodoSSH.Client.Android.Views.LogsScreen"
x:DataType="vm:LogsViewModel"
Background="{StaticResource Canvas}">
<!--
Design 07 — LOGS, under MORE.
Three things on the mock-up are not drawn, and each is the same kind of omission.
◆ **FOLLOW.** The design's header carries a blinking green "FOLLOW" pill and the screen is described as
a live tail over ssh. Nothing here tails anything: these are the keychain's own synced audit records,
written once at close, and they are read when the screen is opened. A pill that blinked would be
animation standing in for a feature. It is a REFRESH button, which is what actually happens.
**The severity filter** — ALL / INFO / WARN / ERROR with counts — has nothing to count. A connection
record is not a syslog line and carries no level. What this screen genuinely has two of is *logs*, so
the design's chip row is spent on the real choice: what was connected to, and what was changed.
**The host chips** are gone for the same reason: nothing filters either log by host.
What is kept is the design's row shape — a quiet timestamp, a coloured word, a source, and the fact
underneath — because that shape is why a log is readable at 10pt on a phone.
-->
<Grid RowDefinitions="Auto,Auto,Auto,*">
<!-- ============ header ============ -->
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto" Height="56" Margin="8,0">
<!--
◆ Gone on a wide surface, where the rail is how you leave and the hub it points at is not drawn at
all. An arrow to a screen nothing else can reach would be the one control on this header that leads
somewhere the layout has removed. See PhoneShell.IsWide.
-->
<Button Grid.Column="0" Classes="icon" Content="←"
IsVisible="{Binding !$parent[views:PhoneShell].IsWide}"
Command="{Binding $parent[views:PhoneShell].((vm:MainWindowViewModel)DataContext).ShowScreenCommand}"
CommandParameter="{x:Static vm:ShellScreen.More}" />
<TextBlock Grid.Column="1" Classes="heading" Text="Logs" Margin="4,0" />
<Button Grid.Column="2" Classes="icon" Content="↻" Command="{Binding RefreshCommand}"
IsEnabled="{Binding !IsBusy}" />
</Grid>
<!-- ============ which log ============ -->
<!--
Mode=OneWay is load-bearing on both of these, not a tidiness: a two-way selection binding moves the
chip before the command it is paired with has had a chance to refuse, so the screen would show a
section it is not on. The keychain's categories carry the same note.
-->
<StackPanel Grid.Row="1" Orientation="Horizontal" Spacing="6" Margin="14,0,14,4">
<RadioButton GroupName="log" Classes="chip" IsChecked="{Binding ShowsConnections, Mode=OneWay}"
Command="{Binding ShowSectionCommand}"
CommandParameter="{x:Static vm:LogSection.Connections}"
Content="CONNECTIONS" />
<RadioButton GroupName="log" Classes="chip" IsChecked="{Binding ShowsActivity, Mode=OneWay}"
Command="{Binding ShowSectionCommand}"
CommandParameter="{x:Static vm:LogSection.Activity}"
Content="KEYCHAIN" />
</StackPanel>
<TextBlock Grid.Row="2" Classes="detail" Margin="18,4,18,6" TextWrapping="Wrap"
Text="{Binding Status}" />
<!-- ============ the log ============ -->
<Border Grid.Row="3" Classes="output" Margin="12,0,12,12">
<Panel>
<!-- ============ what was connected to ============ -->
<!--
One wrapper per section, each holding its own empty state, because "this section is empty" is a
pair of facts — which section, and whether it has rows — and a binding cannot say `ShowsConnections
&& !HasConnections` without a converter. Two Panels are cheaper than a converter and they keep
EmptyMessage, which is already written per section, pointing at the section it describes.
-->
<Panel IsVisible="{Binding ShowsConnections}">
<TextBlock Classes="body" VerticalAlignment="Top" Text="{Binding EmptyMessage}"
IsVisible="{Binding !HasConnections}" />
<ScrollViewer IsVisible="{Binding HasConnections}">
<ItemsControl ItemsSource="{Binding Connections}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:ConnectionLogRowViewModel">
<StackPanel Spacing="1" Margin="0,5">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Classes="detail" FontSize="10" Text="{Binding Started}"
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
<!--
The kind sits where the design puts a severity, because it is the word that tells you
what the row is: a shell or a file transfer. Green while the session is still open —
the same green the host list uses, and the one thing on this screen that is about now
rather than about the past.
-->
<TextBlock Classes="detail" FontSize="10" FontWeight="SemiBold" Text="{Binding Kind}"
Foreground="{StaticResource Info}" VerticalAlignment="Center"
IsVisible="{Binding !IsLive}" />
<StackPanel Orientation="Horizontal" Spacing="5" IsVisible="{Binding IsLive}"
VerticalAlignment="Center">
<Ellipse Classes="dot live" Width="6" Height="6" VerticalAlignment="Center" />
<TextBlock Classes="detail" FontSize="10" FontWeight="SemiBold"
Text="{Binding Kind}" Foreground="{StaticResource Live}" />
</StackPanel>
<TextBlock Classes="detail" FontSize="10" Text="{Binding HostLabel}"
Foreground="{StaticResource TextDim}" VerticalAlignment="Center"
TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" />
</StackPanel>
<TextBlock Classes="detail" FontSize="10.5" Foreground="{StaticResource Text}"
Text="{Binding Address}" TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" />
<!--
TextDim rather than the `detail` class's own faint step. This block is drawn on
TerminalSurface, which is lighter than the Canvas that step was chosen against, so the
same colour that reads as quiet on a screen background reads as barely there here.
-->
<StackPanel Orientation="Horizontal" Spacing="7">
<!-- "still open" rather than a dash, which the plan calls out by name. -->
<TextBlock Classes="detail" FontSize="9.5" Foreground="{StaticResource TextDim}"
Text="{Binding Duration}" />
<TextBlock Classes="detail" FontSize="9.5" Foreground="{StaticResource TextDim}"
Text="·" />
<TextBlock Classes="detail" FontSize="9.5" Foreground="{StaticResource TextDim}"
Text="{Binding DeviceName}" TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" />
</StackPanel>
<!-- A refused host key is the one outcome worth a colour of its own. -->
<TextBlock Classes="detail" FontSize="9.5" IsVisible="{Binding HasOutcome}"
Foreground="{StaticResource DangerText}" Text="{Binding Outcome}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Panel>
<!-- ============ what was changed ============ -->
<Panel IsVisible="{Binding ShowsActivity}">
<TextBlock Classes="body" VerticalAlignment="Top" Text="{Binding EmptyMessage}"
IsVisible="{Binding !HasActivity}" />
<ScrollViewer IsVisible="{Binding HasActivity}">
<ItemsControl ItemsSource="{Binding Activity}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:ActivityLogRowViewModel">
<StackPanel Spacing="1" Margin="0,5">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Classes="detail" FontSize="10" Text="{Binding At}"
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
<TextBlock Classes="detail" FontSize="10" FontWeight="SemiBold"
Text="{Binding Operation}" Foreground="{StaticResource Info}"
VerticalAlignment="Center" />
<TextBlock Classes="detail" FontSize="10" Text="{Binding ItemKind}"
Foreground="{StaticResource TextDim}" VerticalAlignment="Center" />
</StackPanel>
<TextBlock Classes="detail" FontSize="10.5" Foreground="{StaticResource Text}"
Text="{Binding ItemLabel}" TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" />
<!-- The names of the fields that changed, never their values. -->
<TextBlock Classes="detail" FontSize="9.5" IsVisible="{Binding HasChangedFields}"
Foreground="{StaticResource TextDim}"
Text="{Binding ChangedFields}" TextWrapping="Wrap" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Panel>
</Panel>
</Border>
</Grid>
</UserControl>