diff --git a/examples/Console/Paths/Program.cs b/examples/Console/Paths/Program.cs
index 14015fe..41ee0f2 100644
--- a/examples/Console/Paths/Program.cs
+++ b/examples/Console/Paths/Program.cs
@@ -8,13 +8,22 @@ 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 windowsPath = @"C:\This is\A\Super Long\Windows\Path\That\Goes\On And On\And\Never\Seems\To\Stop\But\At\Some\Point\It\Must\I\Guess.txt";
+ var unixPath = @"//This is/A/Super Long/Unix/Path/That/Goes/On And On/And/Never/Seems/To/Stop/But/At/Some/Point/It/Must/I/Guess.txt";
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"));
+ table.AddColumns("[grey]OS[/]", "[grey]Path[/]");
+
+ table.AddRow(new Text("Windows"),
+ new TextPath(windowsPath));
+
+ table.AddRow(new Text("Unix"),
+ new TextPath(unixPath)
+ .RootColor(Color.Blue)
+ .SeparatorColor(Color.Yellow)
+ .StemStyle(Color.Red)
+ .LeafStyle(Color.Green));
+
AnsiConsole.Write(table);
}
}
diff --git a/src/Spectre.Console/Extensions/TextPathExtensions.cs b/src/Spectre.Console/Extensions/TextPathExtensions.cs
new file mode 100644
index 0000000..f118ac1
--- /dev/null
+++ b/src/Spectre.Console/Extensions/TextPathExtensions.cs
@@ -0,0 +1,119 @@
+namespace Spectre.Console;
+
+///
+/// Contains extension methods for .
+///
+public static class TextPathExtensions
+{
+ ///
+ /// Sets the separator style.
+ ///
+ /// The path.
+ /// The separator style to set.
+ /// The same instance so that multiple calls can be chained.
+ public static TextPath SeparatorStyle(this TextPath obj, Style style)
+ {
+ if (obj is null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+
+ obj.SeparatorStyle = style;
+ return obj;
+ }
+
+ ///
+ /// Sets the separator color.
+ ///
+ /// The path.
+ /// The separator color.
+ /// The same instance so that multiple calls can be chained.
+ public static TextPath SeparatorColor(this TextPath obj, Color color)
+ {
+ return SeparatorStyle(obj, new Style(foreground: color));
+ }
+
+ ///
+ /// Sets the root style.
+ ///
+ /// The path.
+ /// The root style to set.
+ /// The same instance so that multiple calls can be chained.
+ public static TextPath RootStyle(this TextPath obj, Style style)
+ {
+ if (obj is null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+
+ obj.RootStyle = style;
+ return obj;
+ }
+
+ ///
+ /// Sets the root color.
+ ///
+ /// The path.
+ /// The root color.
+ /// The same instance so that multiple calls can be chained.
+ public static TextPath RootColor(this TextPath obj, Color color)
+ {
+ return RootStyle(obj, new Style(foreground: color));
+ }
+
+ ///
+ /// Sets the stem style.
+ ///
+ /// The path.
+ /// The stem style to set.
+ /// The same instance so that multiple calls can be chained.
+ public static TextPath StemStyle(this TextPath obj, Style style)
+ {
+ if (obj is null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+
+ obj.StemStyle = style;
+ return obj;
+ }
+
+ ///
+ /// Sets the stem color.
+ ///
+ /// The path.
+ /// The stem color.
+ /// The same instance so that multiple calls can be chained.
+ public static TextPath StemStyle(this TextPath obj, Color color)
+ {
+ return StemStyle(obj, new Style(foreground: color));
+ }
+
+ ///
+ /// Sets the leaf style.
+ ///
+ /// The path.
+ /// The stem leaf to set.
+ /// The same instance so that multiple calls can be chained.
+ public static TextPath LeafStyle(this TextPath obj, Style style)
+ {
+ if (obj is null)
+ {
+ throw new ArgumentNullException(nameof(obj));
+ }
+
+ obj.LeafStyle = style;
+ return obj;
+ }
+
+ ///
+ /// Sets the leaf color.
+ ///
+ /// The path.
+ /// The leaf color.
+ /// The same instance so that multiple calls can be chained.
+ public static TextPath LeafStyle(this TextPath obj, Color color)
+ {
+ return LeafStyle(obj, new Style(foreground: color));
+ }
+}
diff --git a/src/Spectre.Console/Internal/Aligner.cs b/src/Spectre.Console/Internal/Aligner.cs
index 97cda49..92c7ff3 100644
--- a/src/Spectre.Console/Internal/Aligner.cs
+++ b/src/Spectre.Console/Internal/Aligner.cs
@@ -45,7 +45,7 @@ internal static class Aligner
}
}
- public static void Align(RenderContext context, T segments, Justify? alignment, int maxWidth)
+ public static void Align(T segments, Justify? alignment, int maxWidth)
where T : List
{
if (alignment == null || alignment == Justify.Left)
diff --git a/src/Spectre.Console/Widgets/Paragraph.cs b/src/Spectre.Console/Widgets/Paragraph.cs
index bfcc09b..5ac2f8e 100644
--- a/src/Spectre.Console/Widgets/Paragraph.cs
+++ b/src/Spectre.Console/Widgets/Paragraph.cs
@@ -151,7 +151,7 @@ public sealed class Paragraph : Renderable, IAlignable, IOverflowable
{
foreach (var line in lines)
{
- Aligner.Align(context, line, justification, maxWidth);
+ Aligner.Align(line, justification, maxWidth);
}
}
diff --git a/src/Spectre.Console/Widgets/Table/TableRenderer.cs b/src/Spectre.Console/Widgets/Table/TableRenderer.cs
index a0e178a..fadade9 100644
--- a/src/Spectre.Console/Widgets/Table/TableRenderer.cs
+++ b/src/Spectre.Console/Widgets/Table/TableRenderer.cs
@@ -115,7 +115,7 @@ internal static class TableRenderer
}
// Align the row result.
- Aligner.Align(context.Options, rowResult, context.Alignment, context.MaxWidth);
+ Aligner.Align(rowResult, context.Alignment, context.MaxWidth);
// Is the row larger than the allowed max width?
if (Segment.CellCount(rowResult) > context.MaxWidth)
@@ -167,7 +167,7 @@ internal static class TableRenderer
segments.AddRange(((IRenderable)paragraph).Render(context.Options, context.TableWidth));
// Align over the whole buffer area
- Aligner.Align(context.Options, segments, context.Alignment, context.MaxWidth);
+ Aligner.Align(segments, context.Alignment, context.MaxWidth);
segments.Add(Segment.LineBreak);
return segments;
diff --git a/src/Spectre.Console/Widgets/TextPath.cs b/src/Spectre.Console/Widgets/TextPath.cs
index b2acbca..869cb1a 100644
--- a/src/Spectre.Console/Widgets/TextPath.cs
+++ b/src/Spectre.Console/Widgets/TextPath.cs
@@ -5,7 +5,32 @@ namespace Spectre.Console;
///
public sealed class TextPath : IRenderable
{
+ private const string Ellipsis = "...";
+ private const string UnicodeEllipsis = "…";
+
private readonly string[] _parts;
+ private readonly bool _rooted;
+ private readonly bool _windows;
+
+ ///
+ /// Gets or sets the root style.
+ ///
+ public Style? RootStyle { get; set; }
+
+ ///
+ /// Gets or sets the separator style.
+ ///
+ public Style? SeparatorStyle { get; set; }
+
+ ///
+ /// Gets or sets the stem style.
+ ///
+ public Style? StemStyle { get; set; }
+
+ ///
+ /// Gets or sets the leaf style.
+ ///
+ public Style? LeafStyle { get; set; }
///
/// Initializes a new instance of the class.
@@ -20,13 +45,27 @@ public sealed class TextPath : IRenderable
// Get the distinct parts
_parts = path.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
+
+ // Rooted Unix path?
+ if (path.StartsWith("/"))
+ {
+ _rooted = true;
+ _parts = new[] { "/" }.Concat(_parts).ToArray();
+ }
+ else if (_parts.Length > 0 && _parts[0].EndsWith(":"))
+ {
+ // Rooted Windows path
+ _rooted = true;
+ _windows = true;
+ }
}
///
public Measurement Measure(RenderContext context, int maxWidth)
{
var fitted = Fit(context, maxWidth);
- var length = fitted.Sum(f => f.Length) + fitted.Length - 1;
+ var separatorCount = fitted.Length - 1;
+ var length = fitted.Sum(f => f.Length) + separatorCount;
return new Measurement(
Math.Min(length, maxWidth),
@@ -36,19 +75,44 @@ public sealed class TextPath : IRenderable
///
public IEnumerable Render(RenderContext context, int maxWidth)
{
+ var rootStyle = RootStyle ?? Style.Plain;
+ var separatorStyle = SeparatorStyle ?? Style.Plain;
+ var stemStyle = StemStyle ?? Style.Plain;
+ var leafStyle = LeafStyle ?? Style.Plain;
+
var fitted = Fit(context, maxWidth);
-
var parts = new List();
- foreach (var (_, _, last, item) in fitted.Enumerate())
+ foreach (var (_, first, last, item) in fitted.Enumerate())
{
- parts.Add(new Segment(item));
-
- if (!last)
+ // Leaf?
+ if (last)
{
- parts.Add(new Segment("/", new Style(Color.Grey)));
+ parts.Add(new Segment(item, leafStyle));
+ }
+ else
+ {
+ if (first && _rooted)
+ {
+ // Root
+ parts.Add(new Segment(item, rootStyle));
+
+ if (_windows)
+ {
+ // Windows root has a slash
+ parts.Add(new Segment("/", separatorStyle));
+ }
+ }
+ else
+ {
+ // Normal path segment
+ parts.Add(new Segment(item, stemStyle));
+ parts.Add(new Segment("/", separatorStyle));
+ }
}
}
+ parts.Add(Segment.LineBreak);
+
return parts;
}
@@ -66,13 +130,17 @@ public sealed class TextPath : IRenderable
return _parts;
}
- var ellipsis = context.Unicode ? "…" : "...";
+ var ellipsis = context.Unicode ? UnicodeEllipsis : Ellipsis;
var ellipsisLength = Cell.GetCellLength(ellipsis);
if (_parts.Length >= 2)
{
+ var skip = _rooted ? 1 : 0;
+ var separatorCount = _rooted ? 2 : 1;
+ var rootLength = _rooted ? Cell.GetCellLength(_parts[0]) : 0;
+
// Try popping parts until it fits
- var queue = new Queue(_parts.Skip(1).Take(_parts.Length - 2));
+ var queue = new Queue(_parts.Skip(skip).Take(_parts.Length - separatorCount));
while (queue.Count > 0)
{
// Remove the first item
@@ -80,20 +148,27 @@ public sealed class TextPath : IRenderable
// Get the current queue width in cells
var queueWidth =
- Cell.GetCellLength(_parts[0]) // First
+ rootLength // Root (if rooted)
+ ellipsisLength // Ellipsis
+ queue.Sum(p => Cell.GetCellLength(p)) // Middle
+ Cell.GetCellLength(_parts.Last()) // Last
- + queue.Count + 2; // Separators
+ + queue.Count + separatorCount; // Separators
// Will it fit?
if (maxWidth >= queueWidth)
{
var result = new List();
- result.Add(_parts[0]);
+
+ if (_rooted)
+ {
+ // Add the root
+ result.Add(_parts[0]);
+ }
+
result.Add(ellipsis);
result.AddRange(queue);
result.Add(_parts.Last());
+
return result.ToArray();
}
}
@@ -101,7 +176,9 @@ public sealed class TextPath : IRenderable
// 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)) };
+ var take = Math.Min(last.Length, Math.Max(0, maxWidth - ellipsisLength));
+ var start = Math.Max(0, last.Length - take);
+
+ return new[] { string.Concat(ellipsis, last.Substring(start, take)) };
}
}
diff --git a/test/Spectre.Console.Tests/Spectre.Console.Tests.net48.v3.ncrunchproject b/test/Spectre.Console.Tests/Spectre.Console.Tests.net48.v3.ncrunchproject
new file mode 100644
index 0000000..eacd190
--- /dev/null
+++ b/test/Spectre.Console.Tests/Spectre.Console.Tests.net48.v3.ncrunchproject
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/Spectre.Console.Tests/Spectre.Console.Tests.net5.0.v3.ncrunchproject b/test/Spectre.Console.Tests/Spectre.Console.Tests.net5.0.v3.ncrunchproject
new file mode 100644
index 0000000..eacd190
--- /dev/null
+++ b/test/Spectre.Console.Tests/Spectre.Console.Tests.net5.0.v3.ncrunchproject
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/Spectre.Console.Tests/Unit/Widgets/TextPathTests.cs b/test/Spectre.Console.Tests/Unit/Widgets/TextPathTests.cs
index 06ac67f..bddd5b1 100644
--- a/test/Spectre.Console.Tests/Unit/Widgets/TextPathTests.cs
+++ b/test/Spectre.Console.Tests/Unit/Widgets/TextPathTests.cs
@@ -2,32 +2,6 @@ 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")]
@@ -40,6 +14,56 @@ public sealed class TextPathTests
console.Write(new TextPath(input));
// Then
- console.Output.ShouldBe(expected);
+ console.Output.TrimEnd().ShouldBe(expected);
+ }
+
+ [Theory]
+ [InlineData("C:/Foo/Bar/Baz.txt", "C:/Foo/Bar/Baz.txt")]
+ [InlineData("/Foo/Bar/Baz.txt", "/Foo/Bar/Baz.txt")]
+ [InlineData("Foo/Bar/Baz.txt", "Foo/Bar/Baz.txt")]
+ public void Should_Render_Full_Path_If_Possible(string input, string expected)
+ {
+ // Given
+ var console = new TestConsole().Width(40);
+
+ // When
+ console.Write(new TextPath(input));
+
+ // Then
+ console.Output.TrimEnd().ShouldBe(expected);
+ }
+
+ [Theory]
+ [InlineData(17, "C:/My documents/Bar/Baz.txt", "C:/…/Bar/Baz.txt")]
+ [InlineData(15, "/My documents/Bar/Baz.txt", "/…/Bar/Baz.txt")]
+ [InlineData(14, "My documents/Bar/Baz.txt", "…/Bar/Baz.txt")]
+ public void Should_Pop_Segments_From_Left(int width, string input, string expected)
+ {
+ // Given
+ var console = new TestConsole().Width(width);
+
+ // When
+ console.Write(new TextPath(input));
+
+ // Then
+ console.Output.TrimEnd().ShouldBe(expected);
+ }
+
+ [Theory]
+ [InlineData("C:/My documents/Bar/Baz.txt")]
+ [InlineData("/My documents/Bar/Baz.txt")]
+ [InlineData("My documents/Bar/Baz.txt")]
+ [InlineData("Bar/Baz.txt")]
+ [InlineData("Baz.txt")]
+ public void Should_Insert_Line_Break_At_End_Of_Path(string input)
+ {
+ // Given
+ var console = new TestConsole().Width(80);
+
+ // When
+ console.Write(new TextPath(input));
+
+ // Then
+ console.Output.ShouldEndWith("\n");
}
}