DEV Community

Cover image for Spectre.Console Tree widget
Karen Payne
Karen Payne

Posted on

Spectre.Console Tree widget

Introduction

Learn how to use Spectre.Console Tree widget.

The Tree widget visualizes hierarchical data structures with parent-child relationships using Unicode tree characters.

💡 Tree API

Code samples

Are fully documented.

Base example

base sample

public static void Example1()
{
    // Create a tree with a root node
    var tree = new Tree("[deeppink3 bold]root node[/]")
        .Guide(TreeGuide.Line);

    // Add child node to the root node
    TreeNode types = tree.AddNode("[yellow]Types[/] [dim]Child 1[/]");

    // Add grandchild nodes to the child node
    types.AddNode("[green]String[/] [dim]Grandchild 1.1[/]");
    types.AddNode("[green]Int32[/] [dim]Grandchild 1.2[/]");
    types.AddNode("[green]Boolean[/] [dim]Grandchild 1.3[/]");


    // Add child node to the root node
    TreeNode collection = tree.AddNode("[blue]Collections[/] [dim]Child 1[/]");

    // Add grandchild nodes to the child node
    collection.AddNode("[green]List<String>[/] [dim]Grandchild 1.1[/]");
    collection.AddNode("[green]Dictionary<String,Int32>[/] [dim]Grandchild 1.2[/]");

    AnsiConsole.Write(tree);
}
Enter fullscreen mode Exit fullscreen mode

  • First create a new Tree with a colorized label (Initializes a new instance of the Tree class.) and specify line type via Guide (Gets or sets the tree guide lines).
  • Create child nodes off the tree
    • Create child nodes (Grandchild)
  • Use AnsiConsole.Write(tree) to display the tree

Using a Dictionary

This example uses a Dictionary<string, string[]>

results


public static void Example2()
{
    var fileSystem = new Dictionary<string, string[]>
    {
        ["Configuration"] = ["appsettings.json", "secrets.json"],
        ["Source Code"] = ["Main.cs", "Helper.cs", "Utilities.cs"],
        ["Documentation"] = ["README.md", "CHANGELOG.md"],
    };

    var tree = new Tree("[green bold]Application[/]");

    foreach (var (folder, files) in fileSystem)
    {

        var folderNode = tree.AddNode($"[yellow]{folder}[/]");

        foreach (var file in files)
        {
            folderNode.AddNode($"[grey]{file}[/]");
        }

    }

    AnsiConsole.Write(tree);
}
Enter fullscreen mode Exit fullscreen mode
  • Dictionary keys represent child nodes of the tree node
    • Dictionary values represent Grandchild nodes

Using JSON and Grouping

The basics apply as per above, a tree is created followed by child nodes by IEnumerable<IGrouping<string, Book>>

results

Model

public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }
    public decimal? Price { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

internal class BookOperations
{
    public static void BooksGroupings()
    {

        SpectreConsoleHelpers.PrintPink();

        List<Book> books = JsonSerializer.Deserialize<List<Book>>(Json())!;

        IEnumerable<IGrouping<string, Book>> grouped = GroupBooksByPriceRange(books);

        var tree = new Tree("[bold yellow]Books by Price Range[/]");

        foreach (var (price, booksGroup) in grouped.Select(group
                     => (group.Key, group)))
        {
            var priceNode = tree.AddNode($"[bold cyan]{Markup.Escape(price)}[/]");

            foreach (var book in booksGroup)
            {
                priceNode.AddNode(
                    $"[green]Id:[/] {book.Id}, " +
                    $"[green]Title:[/] {Markup.Escape(book.Title)}, " +
                    $"[green]Price:[/] {book.Price:C}");
            }
        }

        AnsiConsole.Write(tree);
    }


    private static string Json()
    {
        var json =
            /* lang=json*/
            """
            [
              {
                "Id": 1,
                "Title": "Learn EF Core",
                "Price": 19.0
              },
              {
                "Id": 2,
                "Title": "C# Basics",
                "Price": 18.0
              },
              {
                "Id": 3,
                "Title": "ASP.NET Core advance",
                "Price": 30.0
              },
              {
                "Id": 4,
                "Title": "VB.NET To C#",
                "Price": 9.0
              },
              {
                "Id": 5,
                "Title": "Basic Azure",
                "Price": 59.0
              }
            ]
            """;

        return json;

    }

   private static IEnumerable<IGrouping<string, Book>> GroupBooksByPriceRange(List<Book> books)
    => books.GroupBy(b => b switch
    {
        { Price: < 10 } => "Under $10",
        { Price: >= 10 and < 20 } => "$10 to $19",
        { Price: >= 20 and < 30 } => "$20 to $29",
        { Price: >= 30 } => "$30 and above",
        _ => "Unknown"
    });
}
Enter fullscreen mode Exit fullscreen mode

EF Core example

This example uses one table to show relations for Employees and Managers.

results

internal class EmployeeOperations
{
    public static void ReportsToManager()
    {

        PrintPink();

        using var context = new Context();

        var table = CreateViewTable();

        List<Employees> employees = [.. context.Employees];

        List<IGrouping<int?, Employees>> groupedData = employees
            .Where(employee => employee.ReportsTo.HasValue)
            .OrderBy(employee => employee.LastName)
            .GroupBy(employee => employee.ReportsTo)
            .ToList();

        List<Manager> managers = [];

        foreach (var group in groupedData)
        {

            Manager manager = new()
            {
                Employee = employees.Find(employee => employee.EmployeeID == group.Key.Value)
            };

            foreach (Employees groupedItem in group)
            {
                manager.Workers.Add(groupedItem);
            }

            managers.Add(manager);

        }

        managers = managers
            .OrderBy(employee => employee.Employee.LastName)
            .ToList();

        var root = new Tree("[white][B]Employees[/][/]");

        foreach (Manager manager in managers)
        {
            var currentNode = root.AddNode($"[Chartreuse1]{manager.Employee.FullName}[/] [Gray37](Manager)[/]");

            foreach (var worker in manager.Workers)
            {
                /*
                 * Here a worker can also be a manager, so we check if the worker is also listed as a manager
                 * in our managers list. If they are, we format their name differently to indicate that
                 * they are both a worker and a manager.
                 */
                if (managers.Any(m => m.Employee.EmployeeID == worker.EmployeeID))
                {
                    currentNode.AddNode($"{worker.FullName} <a href="Manager">Gray37</a>[/]");
                }
                else
                {
                    currentNode.AddNode(worker.FullName);
                }

            }
        }

        AnsiConsole.Write(root);

        Console.WriteLine();

    }

    private static Table CreateViewTable() =>
        new Table()
            .Border(TableBorder.Square)
            .BorderColor(Color.Grey100)
            .Alignment(Justify.Center)
            .Title("~[white on blue][B]Employees[/][/]~")
            .AddColumn(new TableColumn("[u]Manager[/]"))
            .AddColumn(new TableColumn("[u]Workers[/]"));
}
Enter fullscreen mode Exit fullscreen mode

Recursive directory tree with Collapse

Iterates a directory and collapses specific subfolders (obj and bin).

Collapse nodes

public static class DirectoryTree
{
    public static void Display(string path)
    {
        if (!Directory.Exists(path))
        {
            AnsiConsole.MarkupLine("[red]Directory does not exist.[/]");
            return;
        }

        DirectoryInfo root = new(path);

        Tree tree = new($"[bold yellow]{Markup.Escape(root.FullName)}[/]");

        AddDirectory(root, tree);

        AnsiConsole.Write(tree);
    }


    private static void AddDirectory(DirectoryInfo directory, IHasTreeNodes parent)
    {
        bool isBinObj = directory.Name.Equals("obj", StringComparison.OrdinalIgnoreCase) || 
                     directory.Name.Equals("bin", StringComparison.OrdinalIgnoreCase);
        try
        {
            foreach (DirectoryInfo subdirectory in directory.GetDirectories().OrderBy(d => d.Name))
            {
                TreeNode node = parent.AddNode($"[blue]📁 {Markup.Escape(subdirectory.Name)}[/]");

                AddDirectory(subdirectory, node);
                if (isBinObj)
                {
                    node.Collapse();
                }
            }

            if (!isBinObj)
            {
                foreach (FileInfo file in directory.GetFiles().OrderBy(f => f.Name))
                {
                    parent.AddNode($"[green]📄 {Markup.Escape(file.Name)}[/]");
                }

            }


        }
        catch (UnauthorizedAccessException uae)
        {
            parent.AddNode("[red]Access denied[/]");
            Log.Error(uae, "Access denied to directory: {Directory}", directory.FullName);
        }
        catch (IOException ex)
        {
            parent.AddNode($"[red]{Markup.Escape(ex.Message)}[/]");
            Log.Error(ex, "IO exception occurred while accessing directory: {Directory}", directory.FullName);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Main Source code EF Core Source code

Summary

The provided code samples make it easy to create trees.

Top comments (0)