diff --git a/Directory.Packages.props b/Directory.Packages.props index 386b744..440de5f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -100,6 +100,22 @@ ProxyJump both go through a loopback TCP bridge. See docs/adr/. --> + + + + + + + + + + + + + + + + + + + @@ -51,3 +53,4 @@ + diff --git a/src/DodoSSH.Client.Session/WindowsDeviceKeyStore.cs b/src/DodoSSH.Client.App/Platform/WindowsDeviceKeyStore.cs similarity index 91% rename from src/DodoSSH.Client.Session/WindowsDeviceKeyStore.cs rename to src/DodoSSH.Client.App/Platform/WindowsDeviceKeyStore.cs index a396fcd..2feb91f 100644 --- a/src/DodoSSH.Client.Session/WindowsDeviceKeyStore.cs +++ b/src/DodoSSH.Client.App/Platform/WindowsDeviceKeyStore.cs @@ -1,17 +1,28 @@ using System.Runtime.Versioning; using System.Security.Cryptography; +using DodoSSH.Client.Session; -namespace DodoSSH.Client.Session; +namespace DodoSSH.Client.App.Platform; /// -/// Picks the device key store this machine can actually offer. +/// Picks the device key store this desktop machine can actually offer. /// /// +/// /// One place decides, so nothing above has to carry a platform guard. A machine with no TPM, or one that /// is not Windows, gets and therefore keeps asking for the /// passphrase — which is the honest answer rather than a degraded one. +/// +/// +/// "Desktop", because the choice belongs to a head rather than to the session layer. This file used +/// to live in DodoSSH.Client.Session, which was the one thing keeping that project from being +/// portable: everything else in it is platform-neutral, and a Windows CNG dependency in the middle of the +/// vault code meant a second head could not reference it without dragging Windows along. The seam that +/// makes the move free is , which was already there — the session takes a +/// store and has never known which one. See docs/android-port.md. +/// /// -public static class DeviceKeyStores +public static class DesktopDeviceKeyStores { /// The best store this machine supports. public static IDeviceKeyStore ForThisMachine(ClientPaths paths) diff --git a/src/DodoSSH.Client.App/Views/HostSidebar.axaml b/src/DodoSSH.Client.App/Views/HostSidebar.axaml index 0fe36fe..04eeb2d 100644 --- a/src/DodoSSH.Client.App/Views/HostSidebar.axaml +++ b/src/DodoSSH.Client.App/Views/HostSidebar.axaml @@ -36,8 +36,10 @@ + + + @@ -102,11 +131,20 @@ --> + + - + + @@ -148,6 +186,20 @@ + + + + + + + + + IsVisible="{Binding IsConfirmingHostDeletion}"> diff --git a/src/DodoSSH.Client.App/Views/HostsScreen.axaml b/src/DodoSSH.Client.App/Views/HostsScreen.axaml new file mode 100644 index 0000000..35a42db --- /dev/null +++ b/src/DodoSSH.Client.App/Views/HostsScreen.axaml @@ -0,0 +1,252 @@ + + + + + + + + + + + + + + + + + + + + - - - - + + - + + + + + diff --git a/src/DodoSSH.Client.App/Views/TerminalTabs.axaml.cs b/src/DodoSSH.Client.App/Views/TerminalTabs.axaml.cs index ee94c91..797d603 100644 --- a/src/DodoSSH.Client.App/Views/TerminalTabs.axaml.cs +++ b/src/DodoSSH.Client.App/Views/TerminalTabs.axaml.cs @@ -1,9 +1,56 @@ +using Avalonia; using Avalonia.Controls; +using Avalonia.Input; +using DodoSSH.Client.Shell.ViewModels; namespace DodoSSH.Client.App.Views; -/// The tab strip above the terminal. +/// The tab strip, above every screen. internal sealed partial class TerminalTabs : UserControl { public TerminalTabs() => InitializeComponent(); + + /// + /// Closes a tab on a middle click. + /// + /// + /// + /// Wired on the tab's own template root, which is the whole answer to "and not on the strip itself". + /// A middle press on the background, on the sentence, or on the button that opens a connection reaches + /// no handler at all, because there is none there to reach. Nothing has to test what was clicked. + /// + /// + /// PointerUpdateKind, not IsMiddleButtonPressed. The latter reports button + /// state: it is equally true for a left press made while the middle button happens to be held, + /// and for every press during a middle drag. The question here is which button caused this press, and + /// that is the one thing only PointerUpdateKind answers. + /// + /// + /// On press rather than on release, which is what every browser and every terminal does. Matching a + /// release to its press would need capture tracking, to buy the ability to change your mind about a + /// middle click — a gesture nobody makes by accident and nobody aborts. + /// + /// + private void OnTabPointerPressed(object? sender, PointerPressedEventArgs e) + { + if (sender is not Visual { DataContext: TerminalTabViewModel tab } + || DataContext is not MainWindowViewModel shell) + { + return; + } + + if (e.GetCurrentPoint((Visual)sender).Properties.PointerUpdateKind + is not PointerUpdateKind.MiddleButtonPressed) + { + return; + } + + // Handled, so the strip's ScrollViewer does not also take this as the start of a pan. + e.Handled = true; + + // Fire-and-forget, as the host sidebar's double-tap connect is: CloseTabCommand is asynchronous — + // it waits for the workspace to tear the session down — and an event handler has nowhere to await + // it. Its failures are the workspace's to report, not this strip's. + shell.CloseTabCommand.Execute(tab); + } } diff --git a/src/DodoSSH.Client.App/Views/TransfersScreen.axaml b/src/DodoSSH.Client.App/Views/TransfersScreen.axaml index 49a268f..847ba66 100644 --- a/src/DodoSSH.Client.App/Views/TransfersScreen.axaml +++ b/src/DodoSSH.Client.App/Views/TransfersScreen.axaml @@ -65,15 +65,32 @@ - + - + + + @@ -82,18 +92,35 @@ - + + + + + + + + + + + + + +