diff --git a/examples/Console/Paths/Paths.csproj b/examples/Console/Paths/Paths.csproj
new file mode 100644
index 0000000..7729021
--- /dev/null
+++ b/examples/Console/Paths/Paths.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net6.0
+ Paths
+ Demonstrates how to write paths.
+ Widgets
+
+
+
+
+
+
+
diff --git a/examples/Console/Paths/Program.cs b/examples/Console/Paths/Program.cs
new file mode 100644
index 0000000..14015fe
--- /dev/null
+++ b/examples/Console/Paths/Program.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Threading;
+using Spectre.Console;
+
+namespace Live;
+
+public static class Program
+{
+ public static void Main()
+ {
+ AnsiConsole.WriteLine();
+ AnsiConsole.Write(new TextPath(@"C:\Users\Patrik\Source\github\patriksvensson-forks\spectre.console\examples\Console\Paths"));
+ AnsiConsole.WriteLine();
+
+ var table = new Table().BorderColor(Color.Grey);
+ table.AddColumns("[grey]Index[/]", "[yellow]Path[/]");
+ table.AddRow(new Text("1"), new TextPath(@"C:\Users\Patrik\Source\github\patriksvensson-forks\spectre.console\examples\Console\Paths"));
+ AnsiConsole.Write(table);
+ }
+}
diff --git a/examples/Examples.sln b/examples/Examples.sln
index b0bd14d..8bf48eb 100644
--- a/examples/Examples.sln
+++ b/examples/Examples.sln
@@ -67,11 +67,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LiveTable", "Console\LiveTa
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Minimal", "Console\Minimal\Minimal.csproj", "{1780A30A-397A-4CC3-B2A0-A385D9081FA2}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AlternateScreen", "Console\AlternateScreen\AlternateScreen.csproj", "{8A3B636E-5828-438B-A8F4-83811D2704CD}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AlternateScreen", "Console\AlternateScreen\AlternateScreen.csproj", "{8A3B636E-5828-438B-A8F4-83811D2704CD}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spectre.Console", "..\src\Spectre.Console\Spectre.Console.csproj", "{0C58FB17-F60A-47AB-84BF-961EC8C06AE6}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console", "..\src\Spectre.Console\Spectre.Console.csproj", "{0C58FB17-F60A-47AB-84BF-961EC8C06AE6}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Spectre.Console.ImageSharp", "..\src\Spectre.Console.ImageSharp\Spectre.Console.ImageSharp.csproj", "{A127CE7D-A5A7-4745-9809-EBD7CB12CEE7}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console.ImageSharp", "..\src\Spectre.Console.ImageSharp\Spectre.Console.ImageSharp.csproj", "{A127CE7D-A5A7-4745-9809-EBD7CB12CEE7}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Paths", "Console\Paths\Paths.csproj", "{65CB00B0-A3AE-4E8F-A990-4C8C1A232FE2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -479,6 +481,18 @@ Global
{A127CE7D-A5A7-4745-9809-EBD7CB12CEE7}.Release|x64.Build.0 = Release|Any CPU
{A127CE7D-A5A7-4745-9809-EBD7CB12CEE7}.Release|x86.ActiveCfg = Release|Any CPU
{A127CE7D-A5A7-4745-9809-EBD7CB12CEE7}.Release|x86.Build.0 = Release|Any CPU
+ {65CB00B0-A3AE-4E8F-A990-4C8C1A232FE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {65CB00B0-A3AE-4E8F-A990-4C8C1A232FE2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {65CB00B0-A3AE-4E8F-A990-4C8C1A232FE2}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {65CB00B0-A3AE-4E8F-A990-4C8C1A232FE2}.Debug|x64.Build.0 = Debug|Any CPU
+ {65CB00B0-A3AE-4E8F-A990-4C8C1A232FE2}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {65CB00B0-A3AE-4E8F-A990-4C8C1A232FE2}.Debug|x86.Build.0 = Debug|Any CPU
+ {65CB00B0-A3AE-4E8F-A990-4C8C1A232FE2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {65CB00B0-A3AE-4E8F-A990-4C8C1A232FE2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {65CB00B0-A3AE-4E8F-A990-4C8C1A232FE2}.Release|x64.ActiveCfg = Release|Any CPU
+ {65CB00B0-A3AE-4E8F-A990-4C8C1A232FE2}.Release|x64.Build.0 = Release|Any CPU
+ {65CB00B0-A3AE-4E8F-A990-4C8C1A232FE2}.Release|x86.ActiveCfg = Release|Any CPU
+ {65CB00B0-A3AE-4E8F-A990-4C8C1A232FE2}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/src/Spectre.Console/Widgets/TextPath.cs b/src/Spectre.Console/Widgets/TextPath.cs
new file mode 100644
index 0000000..b2acbca
--- /dev/null
+++ b/src/Spectre.Console/Widgets/TextPath.cs
@@ -0,0 +1,107 @@
+namespace Spectre.Console;
+
+///
+/// Representation of a file system path.
+///
+public sealed class TextPath : IRenderable
+{
+ private readonly string[] _parts;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The path to render.
+ public TextPath(string path)
+ {
+ // Normalize the path
+ path ??= string.Empty;
+ path = path.Replace('\\', '/');
+ path = path.TrimEnd('/').Trim();
+
+ // Get the distinct parts
+ _parts = path.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
+ }
+
+ ///
+ public Measurement Measure(RenderContext context, int maxWidth)
+ {
+ var fitted = Fit(context, maxWidth);
+ var length = fitted.Sum(f => f.Length) + fitted.Length - 1;
+
+ return new Measurement(
+ Math.Min(length, maxWidth),
+ Math.Max(length, maxWidth));
+ }
+
+ ///
+ public IEnumerable Render(RenderContext context, int maxWidth)
+ {
+ var fitted = Fit(context, maxWidth);
+
+ var parts = new List();
+ foreach (var (_, _, last, item) in fitted.Enumerate())
+ {
+ parts.Add(new Segment(item));
+
+ if (!last)
+ {
+ parts.Add(new Segment("/", new Style(Color.Grey)));
+ }
+ }
+
+ return parts;
+ }
+
+ private string[] Fit(RenderContext context, int maxWidth)
+ {
+ // No parts?
+ if (_parts.Length == 0)
+ {
+ return _parts;
+ }
+
+ // Will it fit as is?
+ if (_parts.Sum(p => Cell.GetCellLength(p)) + (_parts.Length - 1) < maxWidth)
+ {
+ return _parts;
+ }
+
+ var ellipsis = context.Unicode ? "…" : "...";
+ var ellipsisLength = Cell.GetCellLength(ellipsis);
+
+ if (_parts.Length >= 2)
+ {
+ // Try popping parts until it fits
+ var queue = new Queue(_parts.Skip(1).Take(_parts.Length - 2));
+ while (queue.Count > 0)
+ {
+ // Remove the first item
+ queue.Dequeue();
+
+ // Get the current queue width in cells
+ var queueWidth =
+ Cell.GetCellLength(_parts[0]) // First
+ + ellipsisLength // Ellipsis
+ + queue.Sum(p => Cell.GetCellLength(p)) // Middle
+ + Cell.GetCellLength(_parts.Last()) // Last
+ + queue.Count + 2; // Separators
+
+ // Will it fit?
+ if (maxWidth >= queueWidth)
+ {
+ var result = new List();
+ result.Add(_parts[0]);
+ result.Add(ellipsis);
+ result.AddRange(queue);
+ result.Add(_parts.Last());
+ return result.ToArray();
+ }
+ }
+ }
+
+ // Just trim the last part so it fits
+ var last = _parts.Last();
+ var take = Math.Max(0, maxWidth - ellipsisLength);
+ return new[] { string.Concat(ellipsis, last.Substring(last.Length - take, take)) };
+ }
+}
diff --git a/test/Spectre.Console.Tests/Unit/Widgets/TextPathTests.cs b/test/Spectre.Console.Tests/Unit/Widgets/TextPathTests.cs
new file mode 100644
index 0000000..06ac67f
--- /dev/null
+++ b/test/Spectre.Console.Tests/Unit/Widgets/TextPathTests.cs
@@ -0,0 +1,45 @@
+namespace Spectre.Console.Tests.Unit;
+
+public sealed class TextPathTests
+{
+ [Fact]
+ public void Should_Render_Full_Path_If_Possible()
+ {
+ // Given
+ var console = new TestConsole().Width(40);
+
+ // When
+ console.Write(new TextPath("C:/Foo/Bar/Baz.txt"));
+
+ // Then
+ console.Output.ShouldBe("C:/Foo/Bar/Baz.txt");
+ }
+
+ [Fact]
+ public void Should_Pop_Segments_From_Left()
+ {
+ // Given
+ var console = new TestConsole().Width(17);
+
+ // When
+ console.Write(new TextPath("C:/My documents/Bar/Baz.txt"));
+
+ // Then
+ console.Output.ShouldBe("C:/…/Bar/Baz.txt");
+ }
+
+ [Theory]
+ [InlineData(8, "1234567890", "…4567890")]
+ [InlineData(9, "1234567890", "…34567890")]
+ public void Should_Use_Last_Segments_If_Less_Than_Three(int width, string input, string expected)
+ {
+ // Given
+ var console = new TestConsole().Width(width);
+
+ // When
+ console.Write(new TextPath(input));
+
+ // Then
+ console.Output.ShouldBe(expected);
+ }
+}