Public Access
Merge branch 'claude/host-connection-top-bar-25d04e'
This commit is contained in:
@@ -274,6 +274,12 @@ that design now; the desktop's own v2 is a 190-pixel labelled sidebar in place o
|
||||
search box in the titlebar, and session tabs as pills. Its light theme is not built — see
|
||||
[`docs/design-import-gaps.md`](docs/design-import-gaps.md) — so the application is dark on both.
|
||||
|
||||
**A connected phone shows one bar and then the terminal.** The header, the session strip and the bottom
|
||||
bar are collapsed while a shell is up, and a single 52-pixel row replaces them: back on the left, the
|
||||
sessions as pills, and a `+` on the right offering the three connections this application can make — a
|
||||
shell, a host's files over SFTP, or a bucket. The system back gesture does what the arrow does, and lowers
|
||||
that menu first if it is open.
|
||||
|
||||
Widening the sidebar moved the desktop window's minimum from `880x560` to `1016x574`, which leaves every
|
||||
screen exactly the width it was designed against.
|
||||
|
||||
|
||||
@@ -508,6 +508,14 @@ go at 360dp:
|
||||
drawn, because a modifier that is on and does not look on is how somebody sends `^L` to a database prompt
|
||||
believing they typed an `l`.
|
||||
|
||||
**The surface has since taken the whole screen.** `PhoneShell` collapses the header, the session strip
|
||||
and the bottom bar while a terminal is showing — one binding on `IsShowingPages` each — and the screen
|
||||
draws a 52-pixel bar in their place: back, the session pills, and a `+` raising a sheet with the three
|
||||
connections there are. That sheet is the head's first control that could be drawn over the renderer, so
|
||||
it collapses it rather than covering it, exactly as the desktop's palette does; whether Android's
|
||||
WebView actually composites above Avalonia content is still the unverified question recorded below, and
|
||||
collapsing is correct under either answer.
|
||||
|
||||
All eight are done for the decided scope, and v2 has since gone past it — see the note under step 7.
|
||||
|
||||
What is left, in the order it matters:
|
||||
|
||||
@@ -24,7 +24,7 @@ the chrome, hosts and terminals, file transfer, the vault, teams, and preference
|
||||
> | v2 element | What ships instead |
|
||||
> | --- | --- |
|
||||
> | The **FORWARDING** screen: local/remote/dynamic rules, toggles, bytes transferred | **Nothing, said out loud.** `ISshConnection` offers `OpenShellAsync` and nothing else, so there is no tunnel for a rule to run through; `SyncEntityType.PortForward = 9` is still reserved and still unused. The MORE screen carries a paragraph naming the absence, for the reason the desktop keeps TEAMS in its rail. |
|
||||
> | `23 ms · fwd 5432` on the terminal's connection line | The account and endpoint actually dialled. There is no RTT measurement in SSH.NET and nothing forwards anything. |
|
||||
> | `23 ms · fwd 5432` on the terminal's connection line | ◆ **The line is gone, and what was real on it moved.** There was never an RTT to draw — SSH.NET measures none — and nothing forwards anything, so what shipped was the account and endpoint actually dialled. In v3 a connected phone draws one 52-pixel bar and then the terminal, so a second 36-pixel row naming the machine is exactly the chrome that surface exists to give back: the address is on the connecting card, where it is read before anything has answered, and the shell's own prompt says it afterwards. The two text-size buttons that shared the line are pinned at the end of the accessory row, outside its scroller, which is what the line was protecting them from. |
|
||||
> | `ED25519` badge and `SHA256:kQ9f…Zw2M` on every keychain card | `Detail`, which is what is genuinely known *about* an item. Unchanged from the first import: no algorithm field, no fingerprint, and computing either means parsing armour the type stores verbatim. |
|
||||
> | An `agent` chip on a key | Omitted. There is no agent of any kind — see the first import's Vault section. |
|
||||
> | Snippet cards footed with `edge-eu-1 · today 10:58 · exit 0` | The command and its badge. Nothing records where a snippet ran, when, or what it returned; the shell it is typed into never reports back. |
|
||||
|
||||
@@ -889,3 +889,66 @@ Focus a passphrase box, then turn the phone sideways.
|
||||
|
||||
**Pass:** the box is still visible and still focused, and the shell is intact — the activity handles the
|
||||
rotation rather than being recreated, and live shells survive it.
|
||||
|
||||
---
|
||||
|
||||
## Phase 11 — The phone's terminal surface
|
||||
|
||||
Every check here needs a real device for the reason Phase 10's do, plus one of its own: the interesting
|
||||
question on this screen is whether a native `WebView` composites above what Avalonia draws over it, and no
|
||||
headless surface has a native view to answer with. `docs/android-port.md` still records that as unverified;
|
||||
11.2 is the check that settles it.
|
||||
|
||||
### 11.1 A shell gets the screen · **the important one for chrome**
|
||||
|
||||
Open a shell from HOSTS.
|
||||
|
||||
**Pass:** the vault header, the session strip and the four-entry bottom bar are all gone. What is left is
|
||||
one bar — a back arrow, the session pills, a `+` — and then the terminal down to the accessory keys. Press
|
||||
back: all three come back, the tab is still in the strip and its dot is still green.
|
||||
|
||||
**Failure means:** one of the three rows is not bound on `IsShowingPages`, or the terminal is being reached
|
||||
by a route that leaves `Surface` on `Page`.
|
||||
|
||||
### 11.2 The connect menu is not drawn over the renderer · **the important one**
|
||||
|
||||
With a shell showing output, press `+`.
|
||||
|
||||
**Pass:** the terminal's rectangle goes to the canvas colour and the sheet sits over it whole — scrim, three
|
||||
rows and CANCEL, every one of them tappable, none of them sliced down the left edge. Tap the scrim: the
|
||||
terminal comes back with its scrollback intact and the shell still running.
|
||||
|
||||
**Failure means:** `IsTerminalShowing` is not being cleared by `IsConnectSheetOpen` — or, if the sheet is
|
||||
sliced *despite* the rectangle going blank, something else in that Panel is still showing. A sheet that
|
||||
draws over live terminal output is the Android answer to the compositing question, and it means every
|
||||
future sheet on this surface has to collapse the renderer too.
|
||||
|
||||
### 11.3 Back lowers the menu before it leaves the terminal
|
||||
|
||||
With the connect menu open, use the system back gesture.
|
||||
|
||||
**Pass:** the menu closes and the terminal is still showing. A second back leaves the terminal for the
|
||||
screen it was opened over.
|
||||
|
||||
**Failure means:** the guard in `PhoneShell.axaml.cs` is below the surface check rather than above it, and
|
||||
one gesture is spending two levels.
|
||||
|
||||
### 11.4 The two end buttons cannot be pushed off the bar
|
||||
|
||||
Open six or more shells.
|
||||
|
||||
**Pass:** the pills scroll under a fixed back arrow and a fixed `+`; neither ever leaves the screen, and
|
||||
scrolling the pills to either end does not move them.
|
||||
|
||||
**Failure means:** a control was put inside the `ScrollViewer` rather than beside it.
|
||||
|
||||
### 11.5 The text-size buttons are always reachable
|
||||
|
||||
With a shell open, scroll the accessory key row to the far left and the far right.
|
||||
|
||||
**Pass:** `A−` and `A+` stay pinned at the right-hand end throughout, separated from the keys by the
|
||||
hairline, and both are at least 38 tall. At the smallest and largest sizes the one that can do nothing is
|
||||
visibly disabled rather than silently inert.
|
||||
|
||||
**Failure means:** they have been folded into the scrolling row — which is the arrangement the connection
|
||||
line existed to avoid, and the reason it could be removed at all.
|
||||
|
||||
@@ -21,6 +21,16 @@
|
||||
|
||||
The order is the design's rather than the rail's. Terminal sits second, beside Hosts, because those two
|
||||
are the pair a session moves between; on the desktop the terminal is not a rail entry at all.
|
||||
|
||||
── a terminal gets the screen ─────────────────────────────────────────────────────────────────────────
|
||||
Three of the four rows below stand down while a shell is showing: the header, the shells strip and the
|
||||
bottom bar itself. All three are bound on IsShowingPages, which is the same question asked once — the
|
||||
surface is either a page or a terminal, and these are the chrome a page has.
|
||||
|
||||
The arithmetic is why. Header 56, strip 46, bar 64, and the terminal's own two rows on top of that: at
|
||||
360dp the shell was framed by about a third of the display, all of it about somewhere the user was not.
|
||||
What takes their place is one 52-pixel bar drawn by the surface itself, carrying back on the left and
|
||||
the sessions and a + across from it. See TerminalScreen.axaml.
|
||||
-->
|
||||
|
||||
<!--
|
||||
@@ -49,11 +59,18 @@
|
||||
Hidden behind MORE, and that is the design's arrangement rather than a saving. v2 gives every screen
|
||||
one header carrying that screen's own name and its own actions — a back arrow, an add, a refresh —
|
||||
so the five hub screens draw their own and this one stands down rather than stacking a second row of
|
||||
chrome above theirs. It stays on the three destinations that are the product's top level, where the
|
||||
chrome above theirs. It stays on the two destinations that are the product's top level, where the
|
||||
vault's name and the sync light are the most useful thing a header could say.
|
||||
|
||||
Wrapped rather than given a second condition, because Avalonia's bindings have no "and": the wrapper
|
||||
collapses it over a terminal, where the surface draws its own bar and the vault's name is not what
|
||||
the user is looking at. That is one of three rows this Grid stands down while a shell is showing —
|
||||
see the strip and the bottom bar below.
|
||||
-->
|
||||
<Border Grid.Row="0" Background="{StaticResource Chrome}" BorderBrush="{StaticResource Border}"
|
||||
BorderThickness="0,0,0,1" Padding="14,0" Height="56" IsVisible="{Binding !IsMoreSurface}">
|
||||
<Panel Grid.Row="0" IsVisible="{Binding IsShowingPages}">
|
||||
<Border Background="{StaticResource Chrome}" BorderBrush="{StaticResource Border}"
|
||||
BorderThickness="0,0,0,1" Padding="14,0" Height="56"
|
||||
IsVisible="{Binding !IsMoreSurface}">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
|
||||
|
||||
<!--
|
||||
@@ -72,9 +89,9 @@
|
||||
Text="{Binding Vault.VaultName}" TextTrimming="CharacterEllipsis" />
|
||||
|
||||
<!--
|
||||
The sync light, and it is green only when it has earned it — see SyncLabel. The design draws a
|
||||
permanently green "Synced" here, which is the one claim on that mock-up this application will
|
||||
not make.
|
||||
The sync light, and it is green only when it has earned it — see SyncLabel. The design draws
|
||||
a permanently green "Synced" here, which is the one claim on that mock-up this application
|
||||
will not make.
|
||||
-->
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
|
||||
<Ellipse Classes="dot" Classes.live="{Binding IsFullySynced}" Width="6" Height="6"
|
||||
@@ -84,10 +101,12 @@
|
||||
|
||||
<Button Grid.Column="3" Classes="icon" Margin="4,0,0,0" Command="{Binding LockCommand}"
|
||||
ToolTip.Tip="Lock the keychain">
|
||||
<TextBlock Text="LOCK" Classes="label" FontSize="8.5" Foreground="{StaticResource TextDim}" />
|
||||
<TextBlock Text="LOCK" Classes="label" FontSize="8.5"
|
||||
Foreground="{StaticResource TextDim}" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Panel>
|
||||
|
||||
<!-- The screens. Only one draws; which one is the shell's business. -->
|
||||
<Panel Grid.Row="1">
|
||||
@@ -180,8 +199,14 @@
|
||||
v2 draws the sessions as pills rather than as a labelled row, and drops the word SHELLS: with a
|
||||
rounded chip carrying a live dot and a name, the label was spending nine characters of a 360dp row
|
||||
saying what the row already looks like.
|
||||
|
||||
On every screen except the one it names. The terminal draws these same sessions in its own bar, and
|
||||
two rows of the same pills — one of them 46 pixels of it — is the arrangement this surface exists to
|
||||
stop. Wrapped rather than given a second condition, because the strip's own visibility is about
|
||||
whether there are any tabs and this one is about which surface is up.
|
||||
-->
|
||||
<Border Grid.Row="2" IsVisible="{Binding HasTabs}" Background="{StaticResource Sidebar}"
|
||||
<Panel Grid.Row="2" IsVisible="{Binding IsShowingPages}">
|
||||
<Border IsVisible="{Binding HasTabs}" Background="{StaticResource Sidebar}"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="0,1,0,0" Height="46">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
||||
<ItemsControl ItemsSource="{Binding Tabs}" Margin="12,0" VerticalAlignment="Center">
|
||||
@@ -198,9 +223,9 @@
|
||||
<StackPanel Orientation="Horizontal" Spacing="7" VerticalAlignment="Center">
|
||||
<!--
|
||||
Green only while there is a shell behind the tab. It used to be lit unconditionally,
|
||||
which was true when a tab could not exist without a session; one can now — connecting
|
||||
opens the tab first — and a dot that was green before anything had answered would be
|
||||
the one thing on this strip claiming something untrue.
|
||||
which was true when a tab could not exist without a session; one can now —
|
||||
connecting opens the tab first — and a dot that was green before anything had
|
||||
answered would be the one thing on this strip claiming something untrue.
|
||||
-->
|
||||
<Ellipse Classes="dot" Classes.live="{Binding IsLive}" Width="6" Height="6"
|
||||
VerticalAlignment="Center" />
|
||||
@@ -212,9 +237,19 @@
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</Panel>
|
||||
|
||||
<!-- ============ navigation ============ -->
|
||||
<Border Grid.Row="3" Background="{StaticResource Chrome}" BorderBrush="{StaticResource Border}"
|
||||
<!--
|
||||
Gone while a terminal is showing, which is the whole of that surface's arrangement: the bar's four
|
||||
destinations are replaced by a back arrow and a + that leads to three of them, both in the terminal's
|
||||
own bar. See TerminalScreen.axaml.
|
||||
|
||||
This one is bound directly rather than wrapped — its visibility is a single question and it has no
|
||||
second condition of its own to keep separate.
|
||||
-->
|
||||
<Border Grid.Row="3" IsVisible="{Binding IsShowingPages}"
|
||||
Background="{StaticResource Chrome}" BorderBrush="{StaticResource Border}"
|
||||
BorderThickness="0,1,0,0" Height="64">
|
||||
<Grid ColumnDefinitions="*,*,*,*">
|
||||
|
||||
@@ -225,8 +260,13 @@
|
||||
<!--
|
||||
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.
|
||||
|
||||
The only entry here that never lights, and deliberately no longer tries: this bar is collapsed
|
||||
while the terminal is showing, so IsCurrent could only ever be read as false. Binding it anyway
|
||||
would be a rule about a state this control cannot be in. What marks the terminal as current is
|
||||
the surface filling the screen.
|
||||
-->
|
||||
<views:NavButton Grid.Column="1" Label="Terminal" Glyph="⌗" IsCurrent="{Binding IsTerminalSurface}"
|
||||
<views:NavButton Grid.Column="1" Label="Terminal" Glyph="⌗"
|
||||
Command="{Binding ShowTerminalCommand}" />
|
||||
|
||||
<!--
|
||||
|
||||
@@ -298,6 +298,13 @@ internal sealed partial class PhoneShell : UserControl
|
||||
/// moving between screens at all. Closing an editor is not the same refusal as leaving a host-key
|
||||
/// decision alone — an editor is abandonable by design, and the CANCEL button beside it says so.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>The connect menu is a second such guard, and it matters more than the first.</b> A terminal now
|
||||
/// fills the screen — no header, no bottom bar — so while that menu is up this gesture is the only way
|
||||
/// off it other than the scrim and CANCEL. It is checked before the terminal is dismissed for the
|
||||
/// reason it is drawn over it: back takes the topmost thing, and dismissing the surface underneath a
|
||||
/// menu would take two, neither of them the one being looked at.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
private void OnBackRequested(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
@@ -313,6 +320,17 @@ internal sealed partial class PhoneShell : UserControl
|
||||
return;
|
||||
}
|
||||
|
||||
// The connect menu, which is raised from the terminal's own bar and is the topmost thing the phone
|
||||
// draws while it is up. Ahead of the editors below because it is nearer, and ahead of leaving the
|
||||
// terminal because a gesture that dismissed the surface underneath a menu would close two things at
|
||||
// once — and the one the user was looking at would not be either of them.
|
||||
if (current.IsConnectSheetOpen)
|
||||
{
|
||||
current.CloseConnectSheetCommand.Execute(null);
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryCloseAnOpenEditor(current))
|
||||
{
|
||||
e.Handled = true;
|
||||
|
||||
@@ -16,19 +16,51 @@
|
||||
|
||||
The tab strip is horizontal-scrolling rather than wrapping. Wrapping would reflow the terminal every
|
||||
time a tab opened, which is the one thing a terminal must not do while output is arriving.
|
||||
|
||||
── the screen a shell gets ───────────────────────────────────────────────────────────────────────────
|
||||
A connected phone shows one bar and then the terminal. The vault header, the shells strip and the
|
||||
four-entry bottom bar are all collapsed by PhoneShell while this surface is up, and what replaces them
|
||||
is the row below: back, the sessions, and the way to open another one.
|
||||
|
||||
That is a trade, and the thing bought is the only one a terminal really wants. At 360dp the chrome this
|
||||
screen used to sit inside came to 254 pixels of a roughly 780-pixel display — a third of it — and every
|
||||
one of those rows was about somewhere the user was not. What is given up is the bottom bar's one-tap
|
||||
reach to Hosts, Keychain and MORE; back and the + between them lead to all of it, and the system back
|
||||
gesture does the same thing the arrow does.
|
||||
-->
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,*,Auto">
|
||||
<Panel>
|
||||
|
||||
<!-- ============ tabs ============ -->
|
||||
<Grid RowDefinitions="Auto,*,Auto">
|
||||
|
||||
<!-- ============ the bar ============ -->
|
||||
<!--
|
||||
v2 draws these as pills rather than as a segmented strip, so the row is transparent and each session
|
||||
carries its own outline. The close cross moved inside the pill with the name, which is what makes it
|
||||
read as one object you can dismiss rather than as two adjacent targets.
|
||||
Everything the phone draws above a shell. It is a bar rather than a strip because it now carries the
|
||||
two controls the collapsed chrome took with it, one at each end, with the sessions between them.
|
||||
|
||||
Both are outside the ScrollViewer deliberately. They are the way out of this surface and the way to
|
||||
another host, and a tenth tab must not be able to push either of them off the right-hand edge.
|
||||
-->
|
||||
<Border Grid.Row="0" Height="52">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
||||
<ItemsControl ItemsSource="{Binding Tabs}" Margin="12,0" VerticalAlignment="Center">
|
||||
<Border Grid.Row="0" Height="52" Background="{StaticResource Chrome}"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
|
||||
<!--
|
||||
Back, and it goes to the page this terminal was opened over rather than to Hosts by name. The
|
||||
system back gesture already does exactly that — see PhoneShell.axaml.cs — and an arrow that
|
||||
landed somewhere else would be the second of two answers to one question.
|
||||
-->
|
||||
<Button Grid.Column="0" Classes="icon" Content="←" Margin="4,0,0,0"
|
||||
Command="{Binding ShowScreenCommand}" CommandParameter="{Binding Screen}" />
|
||||
|
||||
<!--
|
||||
v2 draws these as pills rather than as a segmented strip, so the row is transparent and each
|
||||
session carries its own outline. The close cross moved inside the pill with the name, which is
|
||||
what makes it read as one object you can dismiss rather than as two adjacent targets.
|
||||
-->
|
||||
<ScrollViewer Grid.Column="1" HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Disabled">
|
||||
<ItemsControl ItemsSource="{Binding Tabs}" Margin="6,0" VerticalAlignment="Center">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate><StackPanel Orientation="Horizontal" Spacing="6" /></ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
@@ -77,52 +109,26 @@
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<!-- ============ the connection line ============ -->
|
||||
<!--
|
||||
The text-size buttons live here rather than in the accessory row below, and the row is the reason:
|
||||
that one scrolls, so a key can be off-screen, and these two must not be — a terminal that is too
|
||||
small to read is exactly the state in which hunting for the control that fixes it is worst.
|
||||
|
||||
A phone cannot press Ctrl+plus. The desktop head has that chord and needs no buttons; this head has
|
||||
no keyboard to press it with, which is why the two heads differ here and nowhere else in this screen.
|
||||
-->
|
||||
<!--
|
||||
The design's line here also carries a round-trip time and a forwarded port. Neither is drawn: the SSH
|
||||
library offers no RTT measurement, and nothing in this application forwards anything. What is left is
|
||||
the one fact that is real and is the one that matters — the account and endpoint actually dialled.
|
||||
-->
|
||||
<Border Grid.Row="1" Padding="16,5" Height="36"
|
||||
IsVisible="{Binding SelectedTab, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<Grid ColumnDefinitions="*,Auto,Auto">
|
||||
<TextBlock Grid.Column="0" Classes="detail" FontSize="10.5" TextTrimming="CharacterEllipsis"
|
||||
Foreground="{StaticResource TextDim}"
|
||||
VerticalAlignment="Center" Text="{Binding SelectedTab.Address}" />
|
||||
|
||||
<!--
|
||||
Disabled at the ends rather than clamping silently. A button that keeps accepting taps and does
|
||||
nothing reads as the terminal having stopped responding, which is the one thing this screen must
|
||||
never look like.
|
||||
-->
|
||||
<Button Grid.Column="1" Classes="row" MinHeight="34" MinWidth="38" Padding="0"
|
||||
HorizontalContentAlignment="Center"
|
||||
Command="{Binding ShrinkTerminalFontCommand}"
|
||||
IsEnabled="{Binding CanShrinkTerminalFont}">
|
||||
<TextBlock Classes="mono" FontSize="13" Text="A−" />
|
||||
</Button>
|
||||
Another connection, and the three kinds this application can make. A menu rather than a straight
|
||||
jump to Hosts because SFTP and S3 used to be two taps through the bottom bar's MORE, and the bar
|
||||
is not on screen here — so the control that replaces it has to lead to all three or it has quietly
|
||||
removed two of them.
|
||||
|
||||
<Button Grid.Column="2" Classes="row" MinHeight="34" MinWidth="38" Padding="0" Margin="4,0,0,0"
|
||||
HorizontalContentAlignment="Center"
|
||||
Command="{Binding EnlargeTerminalFontCommand}"
|
||||
IsEnabled="{Binding CanEnlargeTerminalFont}">
|
||||
<TextBlock Classes="mono" FontSize="15" Text="A+" />
|
||||
</Button>
|
||||
The desktop's own + refuses a flyout on this reasoning, in TerminalTabs.axaml: a popup dropping
|
||||
into the renderer's rectangle may or may not composite above a native child window, and that is
|
||||
not a claim to make without a screenshot. It is answered here rather than dodged — this is a sheet
|
||||
at the bottom of the screen and opening it collapses the renderer outright, exactly as the
|
||||
palette does on the desktop. Nothing is drawn over the WebView.
|
||||
-->
|
||||
<Button Grid.Column="2" Classes="icon accent" Content="+" Margin="0,0,4,0"
|
||||
Command="{Binding OpenConnectSheetCommand}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ============ the renderer ============ -->
|
||||
<Panel Grid.Row="2">
|
||||
<Panel Grid.Row="1">
|
||||
|
||||
<!--
|
||||
The empty state, and it says what the surface is for rather than that it is empty. A phone opens
|
||||
@@ -131,7 +137,7 @@
|
||||
<StackPanel IsVisible="{Binding !HasTabs}" VerticalAlignment="Center" Margin="24" Spacing="10">
|
||||
<TextBlock Classes="title" FontSize="13" Text="NO SHELL OPEN" />
|
||||
<TextBlock Classes="body"
|
||||
Text="Choose a host and press CONNECT. A shell opened here keeps running while the app is in the background, and keeps running after the keychain is locked — a notification says so for as long as one is alive." />
|
||||
Text="Press + above, or choose a host and press CONNECT. A shell opened here keeps running while the app is in the background, and keeps running after the keychain is locked — a notification says so for as long as one is alive." />
|
||||
</StackPanel>
|
||||
|
||||
<!--
|
||||
@@ -140,10 +146,17 @@
|
||||
hold the application still any more, so there is a stretch in which a tab is selected and there is
|
||||
nothing yet to render in it. A phone needs it more than a desktop does: mobile links are slower, and
|
||||
the alternative is a black rectangle.
|
||||
|
||||
It carries the address, which is where that fact went when the connection line was folded into the
|
||||
bar above. This is the moment it is worth reading — what is being dialled, before anything has
|
||||
answered — and once a shell is open its own prompt says the same thing more accurately than a header
|
||||
derived from the keychain ever did.
|
||||
-->
|
||||
<StackPanel IsVisible="{Binding IsConnectingShowing}" VerticalAlignment="Center" Margin="24"
|
||||
Spacing="10">
|
||||
<TextBlock Classes="title" FontSize="13" Text="{Binding SelectedTab.Label}" />
|
||||
<TextBlock Classes="detail" FontSize="11" Foreground="{StaticResource TextDim}"
|
||||
TextWrapping="Wrap" Text="{Binding SelectedTab.Address}" />
|
||||
<TextBlock Classes="body" Text="{Binding SelectedTab.Status}" />
|
||||
<Button Classes="row" MinHeight="44" Padding="14,0" HorizontalAlignment="Left"
|
||||
Command="{Binding CloseTabCommand}" CommandParameter="{Binding SelectedTab}">
|
||||
@@ -153,13 +166,14 @@
|
||||
|
||||
<!--
|
||||
Collapsed rather than merely covered when there is no pane to show. On Windows this control is a
|
||||
native child window that composites above everything Avalonia draws, which is why the desktop head
|
||||
native child view that composites above everything Avalonia draws, which is why the desktop head
|
||||
hides it explicitly; whether Android's WebView does the same is recorded as unverified in
|
||||
docs/android-port.md. Hiding it either way costs nothing and is correct under both answers.
|
||||
|
||||
IsTerminalShowing rather than HasTabs, which are no longer the same question: a tab that is still
|
||||
connecting has no pane, and showing the renderer for it would show the previous session's output
|
||||
under the name of a machine nothing has connected to yet.
|
||||
under the name of a machine nothing has connected to yet. It is also what the connect sheet turns
|
||||
off — see MainWindowViewModel.IsTerminalShowing.
|
||||
|
||||
v2 insets this behind a 14-pixel radius. Not done, and not an oversight: this is a native child view
|
||||
composited above everything Avalonia draws, so a rounded Border behind it clips nothing — the
|
||||
@@ -178,13 +192,135 @@
|
||||
then release, because holding a modifier while typing is not possible one-thumbed.
|
||||
-->
|
||||
<!-- Only with a pane to type into: the keys send bytes at a session, and a connecting tab has none. -->
|
||||
<Border Grid.Row="3" IsVisible="{Binding IsTerminalShowing}" Height="50">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
|
||||
<Border Grid.Row="2" IsVisible="{Binding IsTerminalShowing}" Height="50">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
|
||||
<ScrollViewer Grid.Column="0" HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Disabled">
|
||||
<StackPanel x:Name="AccessoryKeys" Orientation="Horizontal" Spacing="5" Margin="12,0"
|
||||
VerticalAlignment="Center" />
|
||||
</ScrollViewer>
|
||||
|
||||
<!--
|
||||
The text-size buttons, pinned at this row's right-hand end rather than scrolling with the keys
|
||||
beside them.
|
||||
|
||||
They used to have a row of their own above the terminal, on the argument that the accessory row
|
||||
scrolls and these two must never be off-screen — a terminal too small to read is exactly the state
|
||||
in which hunting for the control that fixes it is worst. That argument is answered rather than
|
||||
abandoned: outside the ScrollViewer they cannot scroll away, and the row they had costs 36 pixels
|
||||
on a surface this change exists to give back.
|
||||
|
||||
A phone cannot press Ctrl+plus. The desktop head has that chord and needs no buttons; this head
|
||||
has no keyboard to press it with, which is why the two heads differ here and nowhere else in this
|
||||
screen.
|
||||
|
||||
Disabled at the ends rather than clamping silently. A button that keeps accepting taps and does
|
||||
nothing reads as the terminal having stopped responding, which is the one thing this screen must
|
||||
never look like.
|
||||
-->
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="5" Margin="8,0,12,0"
|
||||
VerticalAlignment="Center">
|
||||
<Border Width="1" Height="26" Background="{StaticResource Border}" Margin="0,0,3,0"
|
||||
VerticalAlignment="Center" />
|
||||
|
||||
<Button Classes="row" MinHeight="38" Height="38" MinWidth="40" Padding="0" CornerRadius="9"
|
||||
HorizontalContentAlignment="Center"
|
||||
Background="{StaticResource Panel}" BorderBrush="{StaticResource BorderMid}"
|
||||
BorderThickness="1"
|
||||
Command="{Binding ShrinkTerminalFontCommand}"
|
||||
IsEnabled="{Binding CanShrinkTerminalFont}">
|
||||
<TextBlock Classes="mono" FontSize="13" Text="A−" />
|
||||
</Button>
|
||||
|
||||
<Button Classes="row" MinHeight="38" Height="38" MinWidth="40" Padding="0" CornerRadius="9"
|
||||
HorizontalContentAlignment="Center"
|
||||
Background="{StaticResource Panel}" BorderBrush="{StaticResource BorderMid}"
|
||||
BorderThickness="1"
|
||||
Command="{Binding EnlargeTerminalFontCommand}"
|
||||
IsEnabled="{Binding CanEnlargeTerminalFont}">
|
||||
<TextBlock Classes="mono" FontSize="15" Text="A+" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
|
||||
<!-- ============ the connect sheet ============ -->
|
||||
<!--
|
||||
Follows the add sheet on HostsScreen, which follows HostKeySheet: a scrim, a bottom-aligned panel with
|
||||
the top two corners rounded, and a grab handle that is decoration. Dismissible, like the add sheet and
|
||||
unlike the host-key one — "which kind of connection" has no wrong answer and no answer at all is one
|
||||
of them.
|
||||
|
||||
It lives here rather than in PhoneShell for the reason the add sheet lives in its own screen: nothing
|
||||
but this surface raises it. The scrim reaching only the screen area is not a compromise here the way
|
||||
it was there — the bottom bar is collapsed while a terminal is showing, so the screen area is the
|
||||
display.
|
||||
|
||||
Every row navigates away from the terminal. That is not a side effect of the menu, it is the menu:
|
||||
each of the three destinations is a picker, and the shell they open lands back on this surface as a
|
||||
new tab in the bar above.
|
||||
-->
|
||||
<Panel IsVisible="{Binding IsConnectSheetOpen}">
|
||||
|
||||
<!--
|
||||
Declared before the sheet so the sheet draws over it: a Panel stacks its children in declaration
|
||||
order. See the .scrim style for why a tap on it must not light anything up.
|
||||
-->
|
||||
<Button Classes="scrim" Command="{Binding CloseConnectSheetCommand}" />
|
||||
|
||||
<Border VerticalAlignment="Bottom" Background="{StaticResource Panel}"
|
||||
BorderBrush="{StaticResource BorderMid}" BorderThickness="0,1,0,0"
|
||||
CornerRadius="22,22,0,0" Padding="20,18,20,16">
|
||||
<StackPanel Spacing="0">
|
||||
|
||||
<Border Width="38" Height="4" CornerRadius="2" Background="{StaticResource BorderMid}"
|
||||
HorizontalAlignment="Center" Margin="0,0,0,16" />
|
||||
|
||||
<TextBlock Classes="title" Text="CONNECT" FontSize="13" />
|
||||
|
||||
<Button Classes="row" Margin="0,10,0,0" Command="{Binding ShowScreenCommand}"
|
||||
CommandParameter="{x:Static vm:ShellScreen.Hosts}">
|
||||
<StackPanel Spacing="3" VerticalAlignment="Center">
|
||||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="Connect" />
|
||||
<TextBlock Classes="detail" FontSize="10.5"
|
||||
Text="Another shell, on this host or any other in the keychain." />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<!--
|
||||
These two go through ShowFiles rather than ShowScreen, as the MORE hub's own rows do: one screen
|
||||
over one view model, and which kind of remote it offers is the thing being chosen. It can refuse
|
||||
— there is a single transfer session behind both — and refusing lands on the screen the open one
|
||||
belongs to with a sentence saying why, which is a better place to read it than a sheet that has
|
||||
just closed.
|
||||
-->
|
||||
<Button Classes="row" Command="{Binding ShowFilesCommand}"
|
||||
CommandParameter="{x:Static vm:RemoteKind.Host}">
|
||||
<StackPanel Spacing="3" VerticalAlignment="Center">
|
||||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="Connect via SFTP" />
|
||||
<TextBlock Classes="detail" FontSize="10.5" Text="Browse a host's files." />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button Classes="row" Command="{Binding ShowFilesCommand}"
|
||||
CommandParameter="{x:Static vm:RemoteKind.Bucket}">
|
||||
<StackPanel Spacing="3" VerticalAlignment="Center">
|
||||
<TextBlock Classes="mono" FontSize="13.5" FontWeight="SemiBold" Text="Connect via S3" />
|
||||
<TextBlock Classes="detail" FontSize="10.5"
|
||||
Text="Objects in an S3-compatible bucket from the keychain." />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button Classes="secondary" Content="CANCEL" Margin="0,12,0,0"
|
||||
Command="{Binding CloseConnectSheetCommand}" />
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Panel>
|
||||
|
||||
</Panel>
|
||||
|
||||
</UserControl>
|
||||
|
||||
@@ -795,8 +795,15 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
/// and a child window composites above everything its parent paints — so whatever Avalonia draws in the
|
||||
/// same rectangle is drawn underneath it and its buttons cannot be clicked. Anything that covers the
|
||||
/// terminal's area has to collapse the terminal instead, and that is every one of the conditions here: a
|
||||
/// locked vault (the unlock card), the page area (every screen uses the full width), and the
|
||||
/// quick-connect palette.
|
||||
/// locked vault (the unlock card), the page area (every screen uses the full width), the quick-connect
|
||||
/// palette, and the phone's connect sheet.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>The sheet is here rather than in <see cref="IsTerminalSurface"/>, and the palette is not.</b> The
|
||||
/// palette replaces the whole surface, so collapsing everything the terminal half draws is right. The
|
||||
/// sheet is raised from the terminal's own top bar and that bar has to stay on screen behind it —
|
||||
/// dropping the surface would take the bar, the tabs and the phone's whole chrome with it and leave the
|
||||
/// sheet floating over the page underneath. So only the renderer's rectangle is given up.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>The terminal and the pages are exclusive, and that is the whole of the rule.</b> They share one
|
||||
@@ -825,7 +832,8 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
/// safe — that detaches it and destroys the whole WebView2 process tree.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal bool IsTerminalShowing => IsTerminalSurface && SelectedTab is { HasSession: true };
|
||||
internal bool IsTerminalShowing =>
|
||||
IsTerminalSurface && !IsConnectSheetOpen && SelectedTab is { HasSession: true };
|
||||
|
||||
/// <summary>
|
||||
/// Whether the terminal half of the window is the half being shown, pane or no pane.
|
||||
@@ -889,6 +897,52 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
[RelayCommand]
|
||||
private void ShowTerminal() => Surface = ShellSurface.Terminal;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the phone's connect menu is open over the terminal.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Drawn by the Android head alone, and shell state rather than something that view could hold on its
|
||||
/// own for the reason <see cref="IsSearching"/> is: it has to collapse the renderer while it is up. See
|
||||
/// <see cref="IsTerminalShowing"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// It exists because the phone gives a terminal the whole screen. The bottom bar and the vault header
|
||||
/// are gone while a shell is showing, so the three things that bar was the way to — a host, a host's
|
||||
/// files, a bucket — need a way back that is not "leave the terminal first and remember what you were
|
||||
/// doing". The menu is that, and every entry on it is one of the two navigation commands above.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[ObservableProperty]
|
||||
private bool isConnectSheetOpen;
|
||||
|
||||
/// <summary>Raises the connect menu over the terminal.</summary>
|
||||
/// <remarks>
|
||||
/// Gated on the terminal surface rather than merely trusting its only button to be off screen otherwise.
|
||||
/// The flag collapses the renderer, so one set while a page was showing would be a sheet nobody can see
|
||||
/// holding a terminal hidden that nothing would put back.
|
||||
/// </remarks>
|
||||
[RelayCommand]
|
||||
private void OpenConnectSheet()
|
||||
{
|
||||
if (!IsTerminalSurface)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsConnectSheetOpen = true;
|
||||
}
|
||||
|
||||
/// <summary>Lowers the connect menu, leaving the terminal where it was.</summary>
|
||||
/// <remarks>
|
||||
/// The scrim, the CANCEL row and the system back gesture all come here. Choosing an entry does not, and
|
||||
/// does not need to: every entry navigates, and leaving the terminal surface lowers the sheet on its own
|
||||
/// — see <see cref="OnSurfaceChanged"/>, which is what makes "the sheet is only ever up over a terminal"
|
||||
/// true of routes nobody wrote it for.
|
||||
/// </remarks>
|
||||
[RelayCommand]
|
||||
private void CloseConnectSheet() => IsConnectSheetOpen = false;
|
||||
|
||||
// ---- Open terminals ----
|
||||
|
||||
/// <summary>
|
||||
@@ -2533,7 +2587,22 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="OnScreenChanged" />
|
||||
partial void OnSurfaceChanged(ShellSurface value) => RaiseSurfaceState();
|
||||
/// <remarks>
|
||||
/// <b>The one place the connect sheet is lowered by something other than a tap.</b> Every way out of a
|
||||
/// terminal ends here — a rail or bottom-bar destination, the files screen, the palette connecting to a
|
||||
/// host, closing the last tab, a lock — and each of them would otherwise leave the flag set on a shell
|
||||
/// showing a page. That is not merely untidy: the flag collapses the renderer, so the next return to the
|
||||
/// terminal would draw the sheet again over a rectangle held blank by it.
|
||||
/// </remarks>
|
||||
partial void OnSurfaceChanged(ShellSurface value)
|
||||
{
|
||||
if (value is not ShellSurface.Terminal)
|
||||
{
|
||||
IsConnectSheetOpen = false;
|
||||
}
|
||||
|
||||
RaiseSurfaceState();
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Both changes raise the same set, and they have to: <see cref="IsHostsShowing"/> and its four siblings
|
||||
@@ -2596,6 +2665,9 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
|
||||
|
||||
partial void OnIsSearchingChanged(bool value) => RaiseTerminalState();
|
||||
|
||||
/// <inheritdoc cref="OnIsSearchingChanged" />
|
||||
partial void OnIsConnectSheetOpenChanged(bool value) => RaiseTerminalState();
|
||||
|
||||
/// <remarks>
|
||||
/// The unlock card and the confirmation swap, so arming one has to hide the other — see
|
||||
/// <see cref="IsAskingForThePassphrase"/>.
|
||||
|
||||
@@ -682,6 +682,82 @@ public sealed class ShellFlowTests : IAsyncLifetime
|
||||
shell.Screen.ShouldBe(ShellScreen.Vault);
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The phone's connect menu is drawn over the terminal's own rectangle, so it obeys the rule the palette
|
||||
/// does: whatever covers the renderer collapses it instead. The surface stays, because the bar the menu
|
||||
/// was raised from is part of it — see <c>MainWindowViewModel.IsTerminalShowing</c>.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task TheConnectSheet_HidesTheRendererAndLeavesTheSurfaceUnderIt()
|
||||
{
|
||||
var vault = await ReadyToConnectAsync();
|
||||
|
||||
await using var renderer = await FakeRenderer.AttachAsync(workspace, Token);
|
||||
await vault.ConnectCommand.ExecuteAsync(null);
|
||||
|
||||
shell.OpenConnectSheetCommand.Execute(null);
|
||||
|
||||
shell.IsConnectSheetOpen.ShouldBeTrue();
|
||||
shell.IsTerminalShowing.ShouldBeFalse("the sheet draws over the renderer's rectangle");
|
||||
shell.IsTerminalSurface.ShouldBeTrue("the bar the sheet was raised from is on that surface");
|
||||
|
||||
shell.CloseConnectSheetCommand.Execute(null);
|
||||
|
||||
shell.IsTerminalShowing.ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The flag holds the renderer blank, so one set while a page was showing would be a sheet nobody can
|
||||
/// see keeping a terminal hidden that nothing would put back.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task TheConnectSheet_RefusesToOpenOverAPage()
|
||||
{
|
||||
var vault = await ReadyToConnectAsync();
|
||||
|
||||
await using var renderer = await FakeRenderer.AttachAsync(workspace, Token);
|
||||
await vault.ConnectCommand.ExecuteAsync(null);
|
||||
|
||||
shell.ShowScreenCommand.Execute(ShellScreen.Vault);
|
||||
|
||||
shell.OpenConnectSheetCommand.Execute(null);
|
||||
|
||||
shell.IsConnectSheetOpen.ShouldBeFalse();
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Every entry on the menu navigates, and none of them closes the sheet itself: leaving the terminal
|
||||
/// surface is what lowers it. That is the guarantee worth a test — it is what makes routes nobody wrote
|
||||
/// the sheet for, like closing the last tab or locking, safe.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public async Task LeavingTheTerminal_LowersTheConnectSheetHoweverItIsLeft()
|
||||
{
|
||||
var vault = await ReadyToConnectAsync();
|
||||
|
||||
await using var renderer = await FakeRenderer.AttachAsync(workspace, Token);
|
||||
await vault.ConnectCommand.ExecuteAsync(null);
|
||||
|
||||
// The menu's own second entry: one screen, over one view model, with the kind of remote chosen by
|
||||
// the thing that navigates.
|
||||
shell.OpenConnectSheetCommand.Execute(null);
|
||||
shell.ShowFilesCommand.Execute(RemoteKind.Bucket);
|
||||
|
||||
shell.IsConnectSheetOpen.ShouldBeFalse();
|
||||
shell.IsBucketsShowing.ShouldBeTrue();
|
||||
|
||||
// And a route the sheet was never wired to: back to the terminal, open it, then end the only shell
|
||||
// there is.
|
||||
shell.ShowTerminalCommand.Execute(null);
|
||||
shell.OpenConnectSheetCommand.Execute(null);
|
||||
shell.IsConnectSheetOpen.ShouldBeTrue();
|
||||
|
||||
await shell.CloseTabCommand.ExecuteAsync(shell.Tabs[0]);
|
||||
|
||||
shell.IsConnectSheetOpen.ShouldBeFalse("closing the last tab returns the surface to a page");
|
||||
shell.IsShowingPages.ShouldBeTrue();
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// A visible WebView with no pane in it reads as the application having broken, so this is the one
|
||||
/// transition that moves the surface back on its own.
|
||||
|
||||
Reference in New Issue
Block a user