Files
DodoSSH/src/DodoSSH.Client.App/Views/SessionStatusBar.axaml
T
jaap-jan f05deb4fbc
ci / build and test (push) Successful in 3m43s
ci / api image (push) Canceled after 0s
ci / android head (push) Canceled after 4m6s
ci / desktop nightly (push) Canceled after 21s
Add port forwarding: local, remote and dynamic, per terminal session
A host can now carry port forwards (-L, -R, -D), saved inside its
encrypted payload as RelayTicketRequest.PortForwardId already assumed,
so they sync and merge per forward like tags and pinned paths. The host
schema moves to version 8 only for hosts that carry forwards.

ISshConnection gains StartForwardAsync over SSH.NET's forwarded ports.
Every listener binds loopback and there is no bind-address field: a
forward is something any member of a shared keychain can edit. A remote
forward's target is resolved here, IPv4 first, rather than by SSH.NET's
constructor, which takes the first DNS answer and can send ::1.

Forwards start with every terminal opened to the host and stop with its
connection; a reconnect starts them again. The session sidebar lists
them with state, failure reason and an on/off switch, the status bar
counts the listening ones, and the host editor and detail pane gain a
PORT FORWARDS section. A forward that cannot start fails alone.

The phone does not start forwards yet: it has no surface showing what
is listening, and its More screen now says so. No byte counts, since
SSH.NET keeps none.
2026-09-28 20:47:02 +02:00

98 lines
6.8 KiB
XML

<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:DodoSSH.Client.Shell.ViewModels"
x:Class="DodoSSH.Client.App.Views.SessionStatusBar"
x:Name="Root"
x:DataType="vm:MainWindowViewModel">
<!--
── v5b's session shell status bar ───────────────────────────────────────────────────────────────────────
37px, DeepChrome, a 1px top border — the foot of the bordered container both the terminal and the SFTP
surface share. The design's own row also carries a negotiated cipher and a host-key-plus-identity run
(`ed25519 · acme-deploy-key`, clickable to a pin-details modal); both are now real and bound, plumbed all
the way from SSH.NET through TerminalWorkspace.GetSessionFacts and TransfersViewModel to
MainWindowViewModel's own surface-aware properties — see SessionCipher, SessionHostKeyAlgorithm,
SessionIdentityLabel and the composed SessionIdentityText.
Three honest deviations from the mock. The algorithm prints exactly as negotiated — "ssh-ed25519", not
the design's shortened "ed25519" — because trimming it would be a cosmetic edit to a string this client
did not choose. The run is plain text, not a button: this application has no pin-details modal for a
session that is already open, and drawing a click target for a screen that does not exist would be the
fabrication this project's honesty rule forbids, not an honest omission of one. And a typed-password
session — nothing filed in the keychain to name — shows the algorithm alone, with no " · " after it,
because there is no item behind the dot.
What is real and bound: the CONNECTED word and dot, off IsSessionConnected, shown only while there is a
session/host context to report on at all — SessionAddress null means nothing here has anything to say,
the same state the header answers with its own empty-state sentence; the cipher and the host-key/identity
run, each collapsed rather than shown empty or fabricated when the fact behind it is not there — a bucket
connection has neither, and a session whose facts could not be read back at the instant they were asked
for has neither either; the elapsed timer, off SessionElapsedText, which is null and therefore absent
whenever there is nothing timed; and, only on the terminal surface — see <see cref="ShowsEncoding"/> —
"UTF-8", which is a true fact about this client's own renderer and write path (see TerminalWorkspace's
terminal.js and SshShellSessionExtensions.WriteTextAsync) rather than a negotiated session parameter, and
is worded plainly rather than as a claim the remote agreed to it.
-->
<Border Height="37" Background="{StaticResource DeepChrome}"
BorderBrush="{StaticResource Border}" BorderThickness="0,1,0,0">
<Grid ColumnDefinitions="*,Auto" Margin="24,0">
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="18" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal" Spacing="7" VerticalAlignment="Center"
IsVisible="{Binding SessionAddress, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
<Ellipse Classes="dot" Width="7" Height="7" Classes.live="{Binding IsSessionConnected}"
Classes.dropped="{Binding IsSessionDropped}"
VerticalAlignment="Center" />
<TextBlock FontFamily="Montserrat" FontWeight="SemiBold" FontSize="9.5" LetterSpacing="1.2"
VerticalAlignment="Center" Text="CONNECTED" Foreground="{StaticResource Live}"
IsVisible="{Binding IsSessionConnected}" />
<!--
Three words where there were two, and the third replaces rather than joins the second — see
MainWindowViewModel.IsSessionIdle, which is what keeps them from ever being drawn together.
NOT CONNECTED is honest about a tab still dialling and useless about one whose shell has
stopped: it says nothing about which of the two endings it was, and the difference between them
is the difference between work that is finished and a machine that stopped answering. The strip
below carries the sentence and the way back; this is the same fact in the one line that is
always on screen.
-->
<TextBlock FontFamily="Montserrat" FontWeight="SemiBold" FontSize="9.5" LetterSpacing="1.2"
VerticalAlignment="Center" Text="NOT CONNECTED" Foreground="{StaticResource TextFaint}"
IsVisible="{Binding IsSessionIdle}" />
<TextBlock FontFamily="Montserrat" FontWeight="SemiBold" FontSize="9.5" LetterSpacing="1.2"
VerticalAlignment="Center" Text="{Binding SessionEndedText}"
Foreground="{StaticResource TextFaint}"
IsVisible="{Binding SessionEndedText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
</StackPanel>
<TextBlock Classes="mono" FontSize="11.5" Foreground="{StaticResource TextGhost}"
VerticalAlignment="Center" Text="{Binding SessionCipher}"
IsVisible="{Binding SessionCipher, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
<TextBlock Classes="mono" FontSize="11.5" Foreground="{StaticResource TextGhost}"
VerticalAlignment="Center" Text="{Binding SessionIdentityText}"
IsVisible="{Binding SessionIdentityText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="18" VerticalAlignment="Center">
<!--
How many of the selected tab's forwards are listening, and nothing when none are. The terminal's
own, for the reason UTF-8 is: an SFTP connection runs no forwards.
-->
<TextBlock Classes="mono" FontSize="11.5" Foreground="{StaticResource TextGhost}"
VerticalAlignment="Center" Text="{Binding SelectedTab.ForwardsSummary}"
IsVisible="{Binding #Root.ShowsEncoding}" />
<TextBlock Classes="mono" FontSize="11.5" Foreground="{StaticResource TextGhost}"
VerticalAlignment="Center" Text="{Binding SessionElapsedText}"
IsVisible="{Binding SessionElapsedText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
<TextBlock Classes="mono" FontSize="11.5" Foreground="{StaticResource TextGhost}"
VerticalAlignment="Center" Text="UTF-8"
IsVisible="{Binding #Root.ShowsEncoding}" />
</StackPanel>
</Grid>
</Border>
</UserControl>