Skip to content

Commit 4699707

Browse files
authored
Merge pull request #2086 from thomhurst/feature/spectre-dependency-tree
feat: Spectre.Console dependency tree visualization
2 parents 36033fe + 303324f commit 4699707

6 files changed

Lines changed: 292 additions & 42 deletions

File tree

src/ModularPipelines/ConsoleWriter.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Diagnostics.CodeAnalysis;
22
using Spectre.Console;
3+
using Spectre.Console.Rendering;
34

45
namespace ModularPipelines;
56

@@ -19,4 +20,9 @@ public void LogToConsole(string value)
1920
System.Console.WriteLine(value);
2021
}
2122
}
23+
24+
public void Write(IRenderable renderable)
25+
{
26+
AnsiConsole.Write(renderable);
27+
}
2228
}
Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,25 @@
1-
using Microsoft.Extensions.Logging;
21
using Microsoft.Extensions.Options;
32
using ModularPipelines.Interfaces;
43
using ModularPipelines.Options;
4+
using Spectre.Console;
55

66
namespace ModularPipelines.Engine;
77

88
internal class DependencyPrinter : IDependencyPrinter
99
{
1010
private readonly IDependencyChainProvider _dependencyChainProvider;
11-
private readonly ILogger<DependencyPrinter> _logger;
1211
private readonly IConsoleWriter _consoleWriter;
1312
private readonly IBuildSystemFormatter _formatter;
1413
private readonly IOptions<PipelineOptions> _options;
1514
private readonly IDependencyTreeFormatter _treeFormatter;
1615

1716
public DependencyPrinter(IDependencyChainProvider dependencyChainProvider,
18-
ILogger<DependencyPrinter> logger,
1917
IConsoleWriter consoleWriter,
2018
IBuildSystemFormatterProvider formatterProvider,
2119
IOptions<PipelineOptions> options,
2220
IDependencyTreeFormatter treeFormatter)
2321
{
2422
_dependencyChainProvider = dependencyChainProvider;
25-
_logger = logger;
2623
_consoleWriter = consoleWriter;
2724
_formatter = formatterProvider.GetFormatter();
2825
_options = options;
@@ -36,34 +33,31 @@ public void PrintDependencyChains()
3633
return;
3734
}
3835

39-
var formattedTree = _treeFormatter.FormatTree(_dependencyChainProvider.ModuleDependencyModels);
40-
41-
Print(formattedTree);
42-
}
43-
44-
private void Print(string value)
45-
{
46-
if (string.IsNullOrWhiteSpace(value))
36+
var models = _dependencyChainProvider.ModuleDependencyModels;
37+
if (!models.Any())
4738
{
4839
return;
4940
}
5041

51-
_logger.LogDebug("\n");
42+
var tree = _treeFormatter.FormatTree(models);
5243

53-
var startCommand = _formatter.GetStartBlockCommand("Dependency Chains");
44+
Print(tree);
45+
}
46+
47+
private void Print(Tree tree)
48+
{
49+
var startCommand = _formatter.GetStartBlockCommand("Module Dependencies");
5450
if (startCommand != null)
5551
{
5652
_consoleWriter.LogToConsole(startCommand);
5753
}
5854

59-
_logger.LogDebug("The following dependency chains have been detected:\r\n{Chain}", value);
55+
_consoleWriter.Write(tree);
6056

61-
var endCommand = _formatter.GetEndBlockCommand("Dependency Chains");
57+
var endCommand = _formatter.GetEndBlockCommand("Module Dependencies");
6258
if (endCommand != null)
6359
{
6460
_consoleWriter.LogToConsole(endCommand);
6561
}
66-
67-
_logger.LogDebug("\n");
6862
}
6963
}
Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,32 @@
1-
using System.Text;
21
using ModularPipelines.Models;
2+
using Spectre.Console;
33

44
namespace ModularPipelines.Engine;
55

66
/// <summary>
7-
/// Formats dependency trees as text with visual tree structure.
8-
/// Creates a hierarchical representation using box-drawing characters.
7+
/// Formats dependency trees using Spectre.Console Tree widget.
8+
/// Creates a visual hierarchy with proper connectors, colors, and DAG support.
99
/// </summary>
1010
/// <example>
1111
/// <code>
1212
/// // Example output:
13-
/// // ▶ RootModule
14-
/// // └─ DependentModule1
15-
/// // └─ DependentModule2
16-
/// // └─ DependentModule3
13+
/// // Module Dependencies
14+
/// // ├── BuildModule
15+
/// // │ └── CompileModule
16+
/// // │ └── TestModule
17+
/// // └── DeployModule
18+
/// // └── TestModule (↑)
1719
/// //
1820
/// var formatter = new DependencyTreeFormatter();
1921
/// var tree = formatter.FormatTree(rootModules);
20-
/// Console.WriteLine(tree);
22+
/// AnsiConsole.Write(tree);
2123
/// </code>
2224
/// </example>
2325
internal class DependencyTreeFormatter : IDependencyTreeFormatter
2426
{
25-
public string FormatTree(IEnumerable<ModuleDependencyModel> rootModules)
27+
public Tree FormatTree(IEnumerable<ModuleDependencyModel> rootModules)
2628
{
27-
var stringBuilder = new StringBuilder();
29+
var tree = new Tree("[bold]Module Dependencies[/]");
2830
var alreadyPrinted = new HashSet<ModuleDependencyModel>();
2931

3032
foreach (var rootModule in rootModules.OrderBy(m => m.AllDescendantDependencies().Count()))
@@ -34,27 +36,31 @@ public string FormatTree(IEnumerable<ModuleDependencyModel> rootModules)
3436
continue;
3537
}
3638

37-
stringBuilder.AppendLine();
38-
AppendNode(stringBuilder, rootModule, 1, alreadyPrinted);
39+
AddNode(tree, rootModule, alreadyPrinted);
3940
}
4041

41-
return stringBuilder.ToString();
42+
return tree;
4243
}
4344

44-
private void AppendNode(StringBuilder sb, ModuleDependencyModel node, int depth, ISet<ModuleDependencyModel> alreadyPrinted)
45+
private void AddNode(IHasTreeNodes parent, ModuleDependencyModel node, HashSet<ModuleDependencyModel> alreadyPrinted)
4546
{
47+
var isReference = alreadyPrinted.Contains(node);
4648
alreadyPrinted.Add(node);
4749

48-
var indent = new string(' ', (depth - 1) * 2);
49-
var prefix = depth == 1 ? "▶ " : "└─ ";
50+
var moduleName = node.Module.GetType().Name;
51+
var label = isReference
52+
? $"[dim italic]{moduleName}[/] [grey](↑)[/]"
53+
: $"[blue]{moduleName}[/]";
5054

51-
sb.Append(indent);
52-
sb.Append(prefix);
53-
sb.AppendLine(node.Module.GetType().Name);
55+
var treeNode = parent.AddNode(label);
5456

55-
foreach (var dependent in node.IsDependencyFor)
57+
// Don't recurse into references - their children are shown elsewhere
58+
if (!isReference)
5659
{
57-
AppendNode(sb, dependent, depth + 1, alreadyPrinted);
60+
foreach (var dependent in node.IsDependencyFor)
61+
{
62+
AddNode(treeNode, dependent, alreadyPrinted);
63+
}
5864
}
5965
}
6066
}
@@ -65,9 +71,9 @@ private void AppendNode(StringBuilder sb, ModuleDependencyModel node, int depth,
6571
internal interface IDependencyTreeFormatter
6672
{
6773
/// <summary>
68-
/// Formats a collection of root dependency modules as a tree structure.
74+
/// Formats a collection of root dependency modules as a Spectre.Console tree.
6975
/// </summary>
7076
/// <param name="rootModules">The root modules to format.</param>
71-
/// <returns>A formatted string representation of the dependency tree.</returns>
72-
string FormatTree(IEnumerable<ModuleDependencyModel> rootModules);
77+
/// <returns>A Spectre.Console Tree representing the dependency hierarchy.</returns>
78+
Tree FormatTree(IEnumerable<ModuleDependencyModel> rootModules);
7379
}

src/ModularPipelines/IConsoleWriter.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using ModularPipelines.Logging;
2+
using Spectre.Console.Rendering;
23

34
namespace ModularPipelines;
45

@@ -30,4 +31,10 @@ public interface IConsoleWriter
3031
/// </summary>
3132
/// <param name="value">The value to write.</param>
3233
void LogToConsole(string value);
34+
35+
/// <summary>
36+
/// Writes a Spectre.Console renderable to the console.
37+
/// </summary>
38+
/// <param name="renderable">The renderable object to write (Tree, Table, Panel, etc.).</param>
39+
void Write(IRenderable renderable);
3340
}

src/ModularPipelines/Logging/ModuleLogger.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
using ModularPipelines.Console;
44
using ModularPipelines.Engine;
55
using ModularPipelines.Helpers;
6+
using Spectre.Console;
7+
using Spectre.Console.Rendering;
68

79
namespace ModularPipelines.Logging;
810

@@ -54,6 +56,8 @@ internal abstract class ModuleLogger : IInternalModuleLogger, IConsoleWriter
5456

5557
public abstract void LogToConsole(string value);
5658

59+
public abstract void Write(IRenderable renderable);
60+
5761
public void SetException(Exception exception)
5862
{
5963
_exception = exception;
@@ -148,6 +152,20 @@ public override void LogToConsole(string value)
148152
_buffer.WriteLine(obfuscated);
149153
}
150154

155+
public override void Write(IRenderable renderable)
156+
{
157+
// Render to string for buffering, then obfuscate
158+
using var writer = new StringWriter();
159+
var console = AnsiConsole.Create(new AnsiConsoleSettings
160+
{
161+
Out = new AnsiConsoleOutput(writer),
162+
});
163+
console.Write(renderable);
164+
var rendered = writer.ToString();
165+
var obfuscated = _secretObfuscator.Obfuscate(rendered, null) ?? rendered;
166+
_buffer.WriteLine(obfuscated);
167+
}
168+
151169
private Func<object, Exception?, string> MapFormatter<TState>(Func<TState, Exception?, string>? formatter)
152170
{
153171
if (formatter is null)

0 commit comments

Comments
 (0)