diff --git a/src/DodoSSH.Client.Shell/ViewModels/TransfersViewModel.cs b/src/DodoSSH.Client.Shell/ViewModels/TransfersViewModel.cs
index aa072c8..6442662 100644
--- a/src/DodoSSH.Client.Shell/ViewModels/TransfersViewModel.cs
+++ b/src/DodoSSH.Client.Shell/ViewModels/TransfersViewModel.cs
@@ -794,12 +794,48 @@ internal sealed partial class TransfersViewModel : ObservableObject, IAsyncDispo
foreach (var root in LocalDirectory.Roots())
{
- LocalRoots.Add(new CrumbViewModel(root.TrimEnd(Path.DirectorySeparatorChar), root));
+ LocalRoots.Add(new CrumbViewModel(RootChipName(root), root));
}
RefreshLocalCommand.Execute(null);
}
+ /// What a root's chip in the pane header says: C:, /, ~, or a mount's name.
+ ///
+ ///
+ /// A name, never a path — the full path is the chip's and its command
+ /// parameter, and it stays there. This used to be root.TrimEnd(separator), which is a name only
+ /// for a Windows drive: on Unix it made the / chip an empty pill and the home chip the entire
+ /// home path, drawn at full width in a header column nothing bounds. A machine whose home directory sat
+ /// deep enough — CI's per-job HOME is forty-six characters — had that one chip push the header's own
+ /// buttons past the window's edge at the session shell's 472-pixel budget.
+ ///
+ ///
+ /// ~ for home is the one substitution rather than a shortening: every shell a user of this
+ /// application has ever typed into already means "my home directory" by it, which is exactly what the
+ /// chip does when pressed.
+ ///
+ ///
+ internal static string RootChipName(string root)
+ {
+ if (string.Equals(root, LocalDirectory.Home, StringComparison.Ordinal))
+ {
+ return "~";
+ }
+
+ var trimmed = root.TrimEnd(Path.DirectorySeparatorChar);
+ if (trimmed.Length == 0)
+ {
+ // Unix's "/": trimming eats the whole string, and the root's name is the root itself.
+ return "/";
+ }
+
+ // A mount under /media or /run/media names itself by its last segment; a Windows drive ("C:") has
+ // no file-name segment at all, and the trimmed root is already the two-character name it always had.
+ var name = Path.GetFileName(trimmed);
+ return name.Length == 0 ? trimmed : name;
+ }
+
///
/// Gives up the vault, keeping the connection and anything in flight.
///
diff --git a/tests/DodoSSH.Client.App.Tests/RootChipNameTests.cs b/tests/DodoSSH.Client.App.Tests/RootChipNameTests.cs
new file mode 100644
index 0000000..04464af
--- /dev/null
+++ b/tests/DodoSSH.Client.App.Tests/RootChipNameTests.cs
@@ -0,0 +1,47 @@
+using DodoSSH.Client.Shell.ViewModels;
+using DodoSSH.Client.Transfer;
+
+namespace DodoSSH.Client.App.Tests;
+
+/// What the local pane's root chips are called.
+///
+/// A chip carries a name and never a path — the path is the chip's command parameter, where its length costs
+/// nothing. The derivation used to be TrimEnd(separator), which is a name only for a Windows drive:
+/// on Unix it made the / chip an empty pill and the home chip the whole home path, and a home
+/// directory deep enough pushed the pane header's own buttons out of the window — CI's per-job HOME is what
+/// finally said so. These pin the derivation with fixed strings, so the question no longer depends on how
+/// long a path any particular machine keeps its profile under.
+///
+public sealed class RootChipNameTests
+{
+ [Fact]
+ public void TheUnixRootIsASlash_NotAnEmptyPill() =>
+ TransfersViewModel.RootChipName("/").ShouldBe("/");
+
+ ///
+ /// The one substitution rather than a shortening: every shell already means "my home directory" by
+ /// ~, and the machine's real home path — however deep — stays on the chip's command alone.
+ ///
+ [Fact]
+ public void TheHomeChipIsATilde_HoweverDeepHomeSits() =>
+ TransfersViewModel.RootChipName(LocalDirectory.Home).ShouldBe("~");
+
+ [Fact]
+ public void AMountNamesItselfByItsLastSegment() =>
+ TransfersViewModel.RootChipName("/media/usb0").ShouldBe("usb0");
+
+ ///
+ /// Windows only, because the input only exists there — LocalDirectory.Roots() produces drive
+ /// roots on no other platform, and Path.GetFileName reads C:\ differently on Unix.
+ ///
+ [Fact]
+ public void AWindowsDriveKeepsItsTwoCharacterName()
+ {
+ if (!OperatingSystem.IsWindows())
+ {
+ return;
+ }
+
+ TransfersViewModel.RootChipName(@"C:\").ShouldBe("C:");
+ }
+}