📜 ⬆️ ⬇️

CsConsoleFormat: Console formatting in a new way (.NET)

Everyone knows the rich formatting tools in the console: alignment with spaces, changing the current text and background colors. If you want to display a couple of lines, this is completely enough, although the lack of hyphenation in the spaces is sometimes annoying. If you want to display a table, you have to manually calculate the width of the columns, and often just hardcode the width. If you want to color the output, you have to tick the text output with endless switching and color restoration. If you want to display text with hyphenation by words or combine all of the above ...


The code quickly turns into an unreadable mess, in which you cannot disassemble where the logic is, where the text is, where the formatting is. It's horrible! When we write GUI, we have at our disposal all the delights of modern development: MV * patterns, buydings and other cool stuff. After working with a GUI, writing console applications is like returning to the Stone Age.


CsConsoleFormat to the rescue!


Opportunities




Suppose we have the familiar and familiar classes Order, OrderItem, Customer. Let's create a document that displays the order in detail. There are two syntaxes available, we can use any and even combine them.


XAML (a la WPF):


<Document xmlns="urn:alba:cs-console-format" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Span Background="Yellow" Text="Order #"/> <Span Text="{Get OrderId}"/> <Br/> <Span Background="Yellow" Text="Customer: "/> <Span Text="{Get Customer.Name}"/> <Grid Color="Gray"> <Grid.Columns> <Column Width="Auto"/> <Column Width="*"/> <Column Width="Auto"/> </Grid.Columns> <Cell Stroke="Single Wide" Color="White">Id</Cell> <Cell Stroke="Single Wide" Color="White">Name</Cell> <Cell Stroke="Single Wide" Color="White">Count</Cell> <Repeater Items="{Get OrderItems}"> <Cell> <Span Text="{Get Id}"/> </Cell> <Cell> <Span Text="{Get Name}"/> </Cell> <Cell Align="Right"> <Span Text="{Get Count}"/> </Cell> </Repeater> </Grid> </Document> 

C # (a la LINQ to XML):


 using static System.ConsoleColor; var headerThickness = new LineThickness(LineWidth.Single, LineWidth.Wide); var doc = new Document() .AddChildren( new Span("Order #") { Color = Yellow }, Order.Id, "\n", new Span("Customer: ") { Color = Yellow }, Order.Customer.Name, new Grid { Color = Gray } .AddColumns( new Column { Width = GridLength.Auto }, new Column { Width = GridLength.Star(1) }, new Column { Width = GridLength.Auto } ) .AddChildren( new Cell { Stroke = headerThickness } .AddChildren("Id"), new Cell { Stroke = headerThickness } .AddChildren("Name"), new Cell { Stroke = headerThickness } .AddChildren("Count"), Order.OrderItems.Select(item => new[] { new Cell() .AddChildren(item.Id), new Cell() .AddChildren(item.Name), new Cell { Align = HorizontalAlignment.Right } .AddChildren(item.Count), }) ) ); 

Selection of syntax


XAML (a la WPF) makes it clear to separate models and views, which can be considered a virtue. However, XAML is not very strongly typed and does not compile, so runtime errors may occur. The syntax is ambiguous: on the one hand, XML is verbose ( <Grid><Grid.Columns><Column/></Grid.Columns></Grid> ), on the other, it saves on recordings of enumerations ( Color="White" ) and use converters ( Stroke="Single Wide" ).


XAML library in Mono is basic and limited. If you need a cross-platform application, then using XAML can cause problems. However, if you are familiar with WPF, and you only need Windows support, then XAML should be natural. The version for .NET Standard uses the Portable.Xaml library, which should be slightly better, but so far it has not been sufficiently tested in combat conditions.


XAML in general form is only limitedly supported by Visual Studio and ReSharper: although syntax highlighting and code completion usually do not work, do not count on backtracking paths, and errors are sometimes highlighted where there are none. However, for those familiar with XAML, this is nothing new.


C # (a la LINQ to XML) allows you to perform a variety of transformations directly in the code due to LINQ and the collapse of lists when adding sub-elements. If you use C # 6, which supports using static , then you can reduce the record of some enumerations. The only place with non-strict typing is the AddChildren(params object[]) extension method (its use is optional).


The construction of documents in the code is fully supported in any development environment, but attempts to build huge documents with one expression on many pages can lead to brakes when using ReSharper (version 9 sometimes almost hung up the studio; probably now irrelevant).


Real example


The repository on the GitHub has an example of a console application for displaying current processes in the system and launching new processes. It looks like this:



All formatting fits into one small and clear file. Here you can see the output of messages, the output of errors, the formatting of the process table, and the output of help in all possible ways.


To process the command line, the popular CommandLineParser library is used, the BaseOptionAttribute class BaseOptionAttribute from there and contains information about a single command or parameter. Some features of C # 6 are used here. I think the rest of the code does not need any special explanations.


 using System.Collections.Generic; using System.Diagnostics; using System.Linq; using CommandLine; using static System.ConsoleColor; internal class View { private static readonly LineThickness StrokeHeader = new LineThickness(LineWidth.None, LineWidth.Wide); private static readonly LineThickness StrokeRight = new LineThickness(LineWidth.None, LineWidth.None, LineWidth.Single, LineWidth.None); public Document Error (string message, string extra = null) => new Document { Background = Black, Color = Gray } .AddChildren( new Span("Error\n") { Color = Red }, new Span(message) { Color = White }, extra != null ? $"\n\n{extra}" : null ); public Document Info (string message) => new Document { Background = Black, Color = Gray } .AddChildren(message); public Document ProcessList (IEnumerable<Process> processes) => new Document { Background = Black, Color = Gray } .AddChildren( new Grid { Stroke = StrokeHeader, StrokeColor = DarkGray } .AddColumns( new Column { Width = GridLength.Auto }, new Column { Width = GridLength.Auto, MaxWidth = 20 }, new Column { Width = GridLength.Star(1) }, new Column { Width = GridLength.Auto } ) .AddChildren( new Cell { Stroke = StrokeHeader, Color = White } .AddChildren("Id"), new Cell { Stroke = StrokeHeader, Color = White } .AddChildren("Name"), new Cell { Stroke = StrokeHeader, Color = White } .AddChildren("Main Window Title"), new Cell { Stroke = StrokeHeader, Color = White } .AddChildren("Private Memory"), processes.Select(process => new[] { new Cell { Stroke = StrokeRight } .AddChildren(process.Id), new Cell { Stroke = StrokeRight, Color = Yellow, TextWrap = TextWrapping.NoWrap } .AddChildren(process.ProcessName), new Cell { Stroke = StrokeRight, Color = White, TextWrap = TextWrapping.NoWrap } .AddChildren(process.MainWindowTitle), new Cell { Stroke = LineThickness.None, Align = HorizontalAlignment.Right } .AddChildren(process.PrivateMemorySize64.ToString("n0")), }) ) ); public Document HelpOptionsList (IEnumerable<BaseOptionAttribute> options, string instruction) => new Document { Background = Black, Color = Gray } .AddChildren( new Div { Color = White } .AddChildren(instruction), "", new Grid { Stroke = LineThickness.None } .AddColumns(GridLength.Auto, GridLength.Star(1)) .AddChildren(options.Select(OptionNameAndHelp)) ); public Document HelpAllOptionsList (ILookup<BaseOptionAttribute, BaseOptionAttribute> verbsWithOptions, string instruction) => new Document { Background = Black, Color = Gray } .AddChildren( new Span($"{instruction}\n") { Color = White }, new Grid { Stroke = LineThickness.None } .AddColumns(GridLength.Auto, GridLength.Star(1)) .AddChildren( verbsWithOptions.Select(verbWithOptions => new object[] { OptionNameAndHelp(verbWithOptions.Key), new Grid { Stroke = LineThickness.None, Margin = new Thickness(4, 0, 0, 0) } .Set(Grid.ColumnSpanProperty, 2) .AddColumns(GridLength.Auto, GridLength.Star(1)) .AddChildren(verbWithOptions.Select(OptionNameAndHelp)), }) ) ); private static object[] OptionNameAndHelp (BaseOptionAttribute option) => new[] { new Div { Margin = new Thickness(1, 0, 1, 1), Color = Yellow, MinWidth = 14 } .AddChildren(GetOptionSyntax(option)), new Div { Margin = new Thickness(1, 0, 1, 1) } .AddChildren(option.HelpText), }; private static object GetOptionSyntax (BaseOptionAttribute option) { if (option is VerbOptionAttribute) return option.LongName; else if (option.ShortName != null) { if (option.LongName != null) return $"--{option.LongName}, -{option.ShortName}"; else return $"-{option.ShortName}"; } else if (option.LongName != null) return $"--{option.LongName}"; else return ""; } } 

Magic


How is all this piling up of elements being built and transformed into a document? A bird's-eye view:



And now more about each item.


Logical tree


Building a document in XAML resembles WPF, only with {Get Foo} instead of {Binding Foo, Mode=OneTime} and with {Res Bar} instead of {StaticResource Bar} . The converters here are not classes, but single methods that can be accessed via {Call Baz} . Margin and Padding are set, as in WPF, using strings with 1, 2 or 4 numbers. Attached properties are set through the class name and properties through a dot. In short, for those familiar with WPF, everything should be familiar and understandable.


Building a document in C # is done in the spirit of LINQ to XML (System.Xml.Linq), but instead of constructors with the params object[] argument, the AddChildren method (as well as AddColumns ) is used. Here are the available conversions:



For those unfamiliar with WPF, the concept of attached properties may be unusual. The point is that sometimes it is necessary to supplement the already existing properties of elements, for example, a Canvas has both its properties and the properties of coordinates for its elements. These are conveniently set using the Set: new Div(...).Set(Canvas.LeftProperty, 10) extension method.


Visual tree


Elements are divided into two large groups: block and lowercase - just like in early HTML. There are also generator elements (well, that is, one element), which can serve as both, depending on the templates.


The initial tree of elements is logical in terms of WPF, that is, it contains elements in the form in which the programmer created them. Then it turns into the visual irrevocably, that is, it contains elements in the form that is convenient for the engine (roughly speaking, high-level abstractions are converted to elements that can actually display themselves). This conversion includes:



It should also be noted that some elements are able to do more implicit transformations than in WPF, for example, the Grid defaults to using automatic sequential placement of cells, which makes the creation of table rows and specifying the coordinates of each cell unnecessary.


Calculate the size of the elements


All elements recursively call the Measure method, in which the parent informs the child how much free space is assigned to it, and the child answers how much he wants. A child can ask for more than suggested and less, but if you ask for more, the parent will show a piece. At infinity, the parent will also show a fig, even if offered.


Elements with a complex placement logic for children use this stage to perform most of their work (for example, a container of inline elements formats text, taking into account hyphenation and color).


Calculation of the position of elements


All elements are recursively called Arrange, in which each parent is engaged in placing their children.


Rendering elements


All elements are recursively called Render, in which each element displays itself in the virtual console buffer ConsoleBuffer. The ConsoleBuffer class is something like HDC, System.Windows.Forms.Graphics, Graphics.TCanvas, and so on. It contains methods for displaying text, drawing and other things.


Buffer is fed to each element in a convenient form with a limited available area so that you can draw yourself on the coordinates (0; 0) - (Width; Height), without bothering.


Buffer rendering


At this stage the buffer is a rectangular area with text and color. You can either display it in the console, as it was intended, or create something else with it, for example, convert it to HTML or display it in the WPF window. The classes that implement IRenderTarget are responsible for this.


And how difficult is that?


It's simple. Difficult - this is ConsoleFramework and even more so Avalonia. There are no disabilities, no interactivity. Everything is done for the sake of simplicity: and just write documents, and just write elements. All trees are disposable.


All you really need to know is how to use AddChildren (and then to taste), as well as patterns for using basic elements, in particular Grid . Everything else is needed only if you want to create your own elements.


And even if you want to create your own elements, you can restrict yourself to simply converting to already able to display elements, for example, the List is implemented via the Grid, all the logic fits into one trivial method of 16 lines.


And


All this works in .NET 4.0+, .NET Standard 1.3+. For purists, there is a version without XAML support. It is also for conservatives, because it includes support for .NET 3.5.


There is a bag for the support of FIGlet fonts from Colorful.Console, but this dependency was a mistake, because, as it turned out, Colorful.Console is not able to FIGlet in a normal way. Later, there will be either support based on the Figgle package, or its own implementation.


In the repository there are also two example projects and tests with a covering of average scall.


License


Apache 2.0. Some of the code was borrowed from the ConsoleFramework library, written by Igor Kostomin under the MIT license.


Links



')

Source: https://habr.com/ru/post/271299/


All Articles