Content layout

Okay, so we can style text and we can create fancy panels. Cool. Not enough. If we want to get real fancy we need to combine multiple renderable elements. Like this:

import Term
print(Term.make_logo())
╭─ Term.jl ──────────────────────────────────────────────────────────────────────────╮
                                                                                   
          ╭─ Made ───────╮           ╭─ Term.jl ─────────────────────────────────╮ 
               oooo                                                            
            oooooooooo              Term.jl is a Julia package for creating    
           oooooooooooo             styled terminal outputs.                   
           oooooooooooo                                                        
            oooooooooo              Term provides a simple markup language to  
               oooo                 add color and styles to your text.         
          ╰──────────────╯                                                       
 ╭──────────────╮  ╭──────────────╮   More complicated text layout can be        
      oooo            oooo        created using `Renderable` objects such    
   oooooooooo      oooooooooo     as `Panel` and `TextBox`.                  
  oooooooooooo    oooooooooooo    These can also be nested and stacked to    
  oooooooooooo    oooooooooooo    create fancy and informative terminal      
   oooooooooo      oooooooooo     ouputs for your Julia code.                
      oooo            oooo       ╰───────────────────────────────────────────╯ 
 ╰────── with ──╯  ╰─ Term ───────╯                                                
 ────────────────────────────────────────────────────────────────────────────────── 
 Term.jl can also be used to create fancy `logging` and `error` messages.           
                                                                                    
 Check the examples and documentation for more information!                         
 Term.jl is under active development, get in touch for questions or ideas on   how  
 to improve it!                                                                     
╰─────────────────────────────────────────── https://github.com/FedeClaudi/Term.jl ──╯

                  Term.jl is based on the Python library Rich by Will McGugan.
                  https://github.com/Textualize/rich

The example above is composed of panels and textboxes, of course, but also additional lines and spacing elements that can help with the layout. These elements are combined using a very simple syntax to create the whole thing.

Nesting

The easiest way to create a layout is to nest things. We've already seen how to do this with Panels and TextBoxes:

print(
    Panel(
        Panel(
            Panel(
                "We need to go deeper...", height=3, width=28, style="green", box=:ASCII, title="ED", title_style="white"
            ),
            style="red", box=:HEAVY, title="ST", title_style="white"
        ),
        width=44, justify=:center, style="blue", box=:DOUBLE, title="NE", title_style="white"
    )
)
╔═ NE ═════════════════════════════════════╗
     ┏━ ST ━━━━━━━━━━━━━━━━━━━━━━━━━┓     
      +--- ED -------------------+      
      | We need to go deeper...  |      
      +--------------------------+      
     ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛     
╚══════════════════════════════════════════╝

That's all there is really. Panel can take one or multiple string and AbstractRenderable objects as argument and stacks them inside. You can combine this with the width, height and justify argument to mix things up, but simple nesting will only take you so far. We need better way to compose a layout.

Stacking

The idea is simple: horizontally stack two renderables and they will appear side by side, stack them vertically and they will appear one over the other. The syntax is even simpler: * lets you horizontally stack (or concatenate strings) and / lets you stack them vertically.

Note

The choice of * and / as operators for stacking operations was somewhat arbitrary. * was chosen because it's already what Julia uses to concatenate strings, and you can think of that as "horizontally stacking them". / was chosen because it reminds me of fractions, and fractions have one number over another.

If you don't like to use these operators, you're in luck! They are really just a shorthand notation for the functions hstack & vstack. You'll find that this notation makes for some pretty nifty code though.

Let's stack things:

println(
    Panel("horizontally") * Panel("stacked")
)
println("&\n")
println(
    Panel("vertically") / Panel("stacked")
)
╭──────────────╮╭─────────╮
│ horizontally ││ stacked │
╰──────────────╯╰─────────╯

&

╭────────────╮
│ vertically │
╰────────────╯
╭─────────╮
│ stacked │
╰─────────╯

As simple as that. But you can also go crazy if you like:

p = Panel(width=5)
println(
    (p * p * p) / (p * (p/p)) / (p * p * "[bold red]supripse![/bold red]")
)
╭───╮╭───╮╭───╮
╰───╯╰───╯╰───╯
╭───╮╭───╮
╰───╯╰───╯
     ╭───╮
     ╰───╯
╭───╮╭───╮supripse!
╰───╯╰───╯

what's that red text doing in there? We didn't use tprint, or apply_style, we didn't put it into a RenderableText or a TextBox... why didn't it print as "[bold red]supripse![/bold red]"??

The answer is that stacking operators return the generic Renderable type object, and Renderables apply their styles before printing out to console. Okay, not a huge surprise I guess, but I just wanted an excuse to say that regardless of what goes into * and / the output is a generic Renderable (well with the exception of * between two strings which returns a string; also * and / don't work with things like ::Number & co., but you get the idea).

Previously we briefly mentioned the idea of the Measure or a renderable object. Measure stores information about the width and height of a renderable as it will appear in the terminal. When we stack renderables, the Measure of the resulting Renderable will do the following:

  • if we are using * the width will be the sum of widths of the two renderables and the height will be the height of the tallest renderable
  • if we are using / the width will be that of the widest renderable and the height will be the sum of heights.

Let's see:

p1 = Panel(width=5, height=5)
println("p1.measure: ", p1.measure)


p2 = Panel(width=8, height=5)
println("p2.measure: ", p2.measure)

h = p1 * p2
println("* stacked measure: ", h.measure)

v = p1 / p2
println("/ stacked measure: ", v.measure)
p1.measure: Measure (w: 5, h: 5)
p2.measure: Measure (w: 8, h: 5)
* stacked measure: Measure (w: 13, h: 5)
/ stacked measure: Measure (w: 8, h: 10)

This is important, because often you want to know a Renderable's size when creating a layout, as we'll see next.

Spacer

Okay, we can stack two Panels side by side. It looks like this:

p = Panel(width=5, height=3)
print(p * p)
╭───╮╭───╮
│   ││   │
╰───╯╰───╯

but what if we want some space between them? We can do something like

print(p * " "^5 * p)
print(p / "\n"^2 / p)
╭───╮     ╭───╮
│   │     │   │
╰───╯     ╰───╯
╭───╮
│   │
╰───╯



╭───╮
│   │
╰───╯

to create horizontal and vertical spaces. But what if we want to separate two renderables by a space that is 4 characters wide and 3 lines high? We could create a string which does that and stack it with our renderables... Doesn't sound fun. That's why Term has a Spacer renderable object that does it for your:

import Term: Spacer

space = Spacer(5, 3; char=',')
print(p * space * p)
╭───╮,,,,,╭───╮
│   │,,,,,│   │
╰───╯,,,,,╰───╯

here we're using the optional argument char to fill the spacer with a character so that we can see what it looks like. Normally it would be just empty space. The nice thing about spacer is that we can easily do things like this:

top = p * Spacer(5, 3; char='t') * p
mid = Spacer(top.measure.w, 2; char='m') # use top's Measure info !
bottom = p * Spacer(5, 3; char='b') * p

print(top / mid / bottom)
╭───╮ttttt╭───╮
│   │ttttt│   │
╰───╯ttttt╰───╯
mmmmmmmmmmmmmmm
mmmmmmmmmmmmmmm
╭───╮bbbbb╭───╮
│   │bbbbb│   │
╰───╯bbbbb╰───╯

look at that layout! Actually don't, look at it without that clutter:

top = p * Spacer(5, 3) * p
mid = Spacer(top.measure.w, 2) # use top's Measure info !
bottom = p * Spacer(5, 3) * p

print(top / mid / bottom)
╭───╮     ╭───╮
│   │     │   │
╰───╯     ╰───╯


╭───╮     ╭───╮
│   │     │   │
╰───╯     ╰───╯

vLine

Space is nice. You can separate distinct pieces of content so that the message you're trying to convey is more easily interpreted by the user. But space is not enough. Sometimes you want to add a line to mark out where one section ends and the other starts. Well, that's where vLine and hLine below come in. They're very simple to use, just say how tall/weide the line should be and, optionally, give some markup style information too:

import Term: vLine

l = vLine(p.measure.h; style="bold red")
s = Spacer(2, p.measure.h)

print(p * l * s * l * p)
╭───╮  ╭───╮
│   │  │   │
╰───╯  ╰───╯
Note

Like with Panel, vLine and hLine accept a box= keyword argument with the ::Symbol of any of the Box objects supported by Term.

hLine

I think you can guess where we are going with this. hLine is just like vLine but horizontal:

import Term: hLine

l = hLine(20, "whaaat"; style="bold red", box=:DOUBLE)


print(p / l / p)
╭───╮
│   │
╰───╯
═══════ whaaat ════
╭───╮
│   │
╰───╯

surprise! hLine is not just like vLine: it also accepts an optional text argument to create a little title line if you will. But yeah, otherwise it's just the same.

With this we conclude our overview of the content layout in Term. There's a lot more you can use Term for, but styled text, panels and layout operators will get you far! Enjoy!