Concepts - uhop/console-toolkit GitHub Wiki
The toolkit was built out of frustration with the current state of the CLI interfaces. Every time I wrote a new CLI utility I faced an uphill battle to create a compelling user experience.
So the toolkit was born. It makes it easy to paint things in the terminal and provides common functionality in a cohesive way:
- Support for ANSI escape sequences, mainly CSI and SGR.
- Colors, font styles, etc.
- Bold, dim, italic, underline, etc.
- Handling the cursor and the screen.
- Data-driven charts and tables with themes.
- Bitmap-based plotting and turtle graphics.
- Output facilities for creating updatable displays.
- And more.
Foundations
The toolkit is build around three main concepts:
- Unicode.
- ANSI escape sequences (CSI and SGR).
- Text containers are based on a notion of rectangular boxes.
The toolkit works with the following text containers:
strings— an array of strings. Implemented in strings.js.Box— a rectangular box. It is an object with aboxproperty. Theboxis essentiallystringsbut all strings are of the same size. All smaller strings are padded with spaces or a character of your choice. Implemented in box.js.Panel— a 2D array where each element is an object containing a character and a SGR state. Implemented in panel.js.
There is a simple way to convert between the three text containers.
All toolkit's facilities produce text containers of some sort. Objects implement toStrings(),
toBox(), and toPanel() methods or a combination of them.
Strings
This is an ad-hoc object implemented as an array of strings. Individual strings cannot have newlines or tabs in them, but they can have Unicode symbols and SGR sequences.
The strings are very easy to work with:
const strings = ['one', 'two', 'three'];
for (const line of strings) console.log(line);
strings are usually handled as immutable objects.
The module provides a way to clip strings to a given length.
getLength(string)
The module provides the getLength(string) function that correctly calculates the length of
a string taking into account Unicode code points that take up multiple UTF16 items and excluding
unprintable ANSI escape sequences.
Box
Conceptually Box is strings on steroids. All strings are the same size (see getLength() above).
When converting from strings to Box, all smaller strings will be padded with a character and
an alignment of your choice.
Box provides a some efficient methods to manipulate it:
- Various padding methods.
- Stacking two boxes horizontally or vertically.
- Removing rows.
- Reversing vertically.
Boxes are usually handled as immutable objects.
Panel
Panel is a 2D array where each element is an object containing a character and a SGR state.
It provides a plethora of methods to manipulate it:
- Extraction of a rectangular region as a panel.
- Copying a rectangular region from one panel to another.
- Copying a text container to a panel at a given position.
- Applying a function to a rectangular region of the panel.
- Filling a rectangular region with a character and/or SGR state.
- Combine individual states with an external state within a rectangular region.
- Various padding methods.
- Removing and inserting rows and columns.
- Resizing.
- Stacking with another panel horizontally or vertically.
- Transformations: transpose, rotate 90°, rotate 180°, rotate 270°, flip horizontally, flip vertically.
Panels are usually modified in place.
When converting from strings or Box to Panel, all SGR sequences will be interpreted and assigned
to the appropriate cells as state objects. When converting from Panel to strings or Box, all SGR
sequences will be recreated from states.
ANSI escape sequences
The toolkit provides ANSI escape sequences in the form of CSI and SGR sequences.
Technically, SGR is a subset of CSI. It governs how text is printed on the screen with the following features:
- Colors: standard 16 colors, 256 colors, and true colors.
- Bold, dim, italic, underline, strike-through, blinking, inverse text.
The rest of CSI sequences deal with the cursor and the screen.
Unicode
The toolkit is all about precise positioning of text on the screen. Unfortunately, JavaScript
strings do not provide a simple way to calculate how many positions a string occupies because
some characters may take up two UTF16 items. The length property is useless here.
The string should be interpreted and positions are calculated.
Note that Unicode is used not only to write some languages but for symbols and emojis we want to use to make user experience better.
The getLength() function described above is used to calculate the length of a string taking into account
Unicode code points correctly.
Double-wide symbols
Some Unicode characters are double-wide. On a screen with a monospaced font, they take up two positions. Unfortunately, it depends on the font and how it is designed so while it is possible to calculate the length, it is not possible to display them correctly on a screen with the 100% certainty.
By default the toolkit assumes that all characters are single-wide. But it can use the third-party packages to support double-wide characters:
- emoji-regex to detect double-wide emojis.
- get-east-asian-width for East Asian characters.
If those packages are present, the toolkit will use them to calculate the length correctly. It is possible to install any of them or both because they are used independently.
Warning: these packages are not 100% reliable and not guaranteed to work in all cases. They are not
listed as the dependencies in package.json. You should install them explicitly.
Note on Bun: getLength() function uses Bun.stringWidth() automatically.
Unification of arguments
If a function assumes that an argument is strings, or a box, or a panel, it passes the argument through a special function responsible for converting it to strings (or a box, or a panel). The same is true for an SGR state. The objects, the functions and their source:
| Object | Function | Source |
|---|---|---|
strings |
toStrings() |
strings.js |
Box |
toBox() |
box.js |
Panel |
toPanel() |
panel.js |
| SGR state | toState() |
ansi/sgr-state.js |
Any object can provide a conversion to these objects. For that they have to implement the following methods:
| Required object | Method |
|---|---|
strings |
toStrings() |
Box |
toBox() |
Panel |
toPanel() |
| SGR state | toState() |
Usually it is not required to implement all text container methods. Even a single one will be enough. It makes sense to implement only the methods that can be implemented efficiently relying on generic conversion for the rest.