One of the goals of Junction is to make handling formatting and styling in terminal-based applications simpler by working at a higher level than the raw escape-sequence information provided by librarise like curses and blessings. However, like all things which work some magic to make life simpler for you, it can pay to understand a little of how they’re put together so that you can work with them more easily.
The first concept it’s useful to draw out is the distinction in Junction between a format and a style. This is akin to the distinction one would see in a word processor, where individual formatting notions like ‘red’, ‘bold’ or ‘underline’ can be applied to a portion of text, but styles with semantic meaning like ‘heading’, ‘paragraph’ or ‘table’ can be defined that encapsulate those specific formats.
This encapsulation makes maintenance of large documents (or in our case applications) simpler, because if you want to change what headings look like, you simply redefine what ‘heading’ means, rather than doing a painful find-and-replace operation.
We make this distinction first because throughout this document the word ‘format’ refers to a specific formatting instruction with a concrete look that can be applied to your text, whilst the word ‘style’ refers to a named entity that can reference any number of formats (or indeed, other styles).
So how does one go about specifying some formatted text using Junction? Let’s look at examples/quick_brown_fox.py for a quick overview:
1 2 3 4 5 6 7 8 9 10 11 | from jcn import Root, Text
# The resulting StringWithFormatting passed to the Text element constructor
# below illustrates how we try to match up to blessings' ways of doing
# formatting.
text = Text(
Root.format.bold('The ') + 'quick ' +
Root.format.red('brown') + ' fox ' + Root.format.underline('jumps') +
Root.format.reverse(Root.format.green(' over ') + 'the lazy dog'))
root = Root(text)
root.run()
|
In lines 7-9, we can see some content strings and access to some attributes with names like ‘bold’, ‘red’ and ‘underline’. For those of you who are familiar with blessings, this pattern of attribute access should look familiar, and the names for the terminal formats are identical. Any format that can be accessed on a blessings.Terminal can be access via jcn.Root.format. Notice how you construct a formatted string by adding together blocks of content formatted by the attributes accessed via Root.format and/or regular strings.
Whilst our behaviour is like blessings‘, we have some additional behaviour and restrictions of which you need to be aware:
Styles are built on top of formats. They are similar in how they are inserted into content strings and how they are drawn to the screen (ultimately the resolve to a series of terminal escape sequences, just like formats). However, they provide you with a way to give some semantic meaning to the formatting that you want to use in your application. Let’s look at examples/styles.py for an example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | from jcn import Root, Text
Root.style.heading = Root.format.underline
Root.style.h1 = Root.style.heading + Root.format.bold
Root.style.h2 = Root.style.heading + Root.format.color(240)
text = Text(
Root.style.h1('Disclaimer:\n') +
"This software comes with absolutely no warranty, not even for "
"merchantability or fitness for a particular purpose\n\n" +
Root.style.h2('Footnote:\n') +
"But we've made every effort to make it awesome ;-)")
root = Root(text)
root.run()
|
In lines 4-6 we can see how named styles can be assigned as combinations of other formats and styles. You can name any style you like by assigning to Root.style.name. The example shows how it can be useful to make style heirarchical, so more specific style refer to more general ones. We see that all ‘headings’ are underlined, but ‘h1’ is bold and ‘h2’ has a specific colour (dark grey, for those of you taking notes ;-) ).
Styles are used in lines 9-13 in the same way as formats: the are accessed as attributes, applied by calling them on their content and combined into a StringWithFormatting by addition.
The formatting and styling API of Junction is designed to be reasonably straight-forward and intuitive, whilst providing intellegence internally when calculating UI layout. There’re quite a few classes involved in the process to cover a variety of circumstances and help us build our formatting concepts up logically. This section is presented for any interested parties, but you shouldn’t need to understand it as a regular user.
The most basic building block of our formatting logic is the Placeholder. A Placeholder is literally used in a StringWithFormatting in place of a real escape sequence value. This is important, because our application content can be defined independently of when it is drawn to the screen. (Imagine the case where someone changes the look of a ‘heading’ mid-application.)
We define four different types of Placeholder: FormatPlaceholer, ParameterizingFormatPlaceholder, NullPlaceholder (which is available as the singleton jcn.null_placeholder) and StylePlaceholder. These placeholders reference formats, formats that take arguments (such as Root.color), the ‘unformatted’ or ‘normal’ format, and styles respectively. We also define a fifth class, PlaceholderGroup, which acts similarly to Placholder, but is a container for multiple placholders. Placholder objects are combined by addition to form PlaceholderGroup objects.
Instances of Placholder are generated for the user by FormatPlaceholderFactory and StylePlaceholderFactory, which are presented to the user as Root.format and Root.style respectively. In addition to generating StylePlaceholder objects, the StylePlaceholderFactory also coordinates registration of user-defined styles and provides internal access to those definitions when they are needed. That is, the user can make statements like:
Root.style.button = Root.format.bold + Root.format.green
for later retrieval.
At draw time, Placeholders are replaced with escape squence data before strings are passed to a Terminal for drawing. This process, which is mirrored by other objects defined in formatting.py is performed by Placholder.populate(). Populate requires a Terminal from which to look up terminal escape sequences, and a StylePlaceholderFactory object, which will resolve styles in a similar manner.
Where Placeholder objects were the most-basic of our formatting constructs, at the other end of the spectrum we have StringWithFormatting. This class endeavours to be everything a string can be, with the added bonus of being able to attach formatting information to arbitrary portions of the string. Inevitably this means there is a little bit of magic going on behind the scenes!
At a fundamental level, StringWithFormatting is a simple container for chunks of string which each have a single format. The StringWithFormatting.populate() method, analgous to Placeholder.populate, traverses the contents of the StringWithFormatting object and populates each portion of the string in turn.
Because a StringWithFormatting is essentially a list of component strings, any str methods that get applied to a StringWithFormatting need to be applied over the whole list of string components, which can complicate matters somewhat. For example, str.stip() should only affect each end of a str, so StringWithFormatting has to be careful not to apply str.strip() equally every single one of its components.
The final class of object that completes the formatting puzzle is the StringComponent. Each StringComponent is a specification of what each portion of a StringWithFormatting is like. It is a simple derivative of str that also maintains a reference to a Placholder instance for instruction as to how that content should be formatted. Values return from calling methods of str are handled such that the StringComponent object’s placeholder is attached appropriately. When populated (via StringComponent.populate() in much the same vein as our other classes), a StringComponent returns the string’s contents prepended by the formatting escape sequences.
StringComponent objects are created by the user when they call Placeholder objects with some content. StringWithFormatting objects are then created when StringComponent objects are added together. This makes a nice, seamless API where users don’t have to keep track of all of our internal semantics, whilst giving us several useful levels of abstraction for dealing with the problem, which turns out to be quite complex(!), of how to draw escape sequences to the screen in a sensible order.