Examples

Layouting Examples

Visual Demo

from functui import Rect, layout_to_result
from functui.flex import flex, flex_custom, hbox_flex_wrap, hbox_flex, vbox_flex
from functui.common import *
from functui.classes import *
from functui.io.ansi import result_to_str
from functui.io.html import result_to_html_str
from itertools import batched

from functui.rich_text import adaptive_text
import math


def display_char_styles():
    out = []
    for i in StyleAttr:
        out.append(text(i.name) | push_rule(StyleRule(add_attrs=i)))
    return hbox(intersperse(out, text(" ")))

def cell_white_text(color8: int):
    return text(f"{color8: >3}") | padding | bg_fill | bg(color8) | fg(Color4.BRIGHT_WHITE)
def cell_black_text(color8: int):
    return text(f"{color8: >3}") | padding | bg_fill | bg(color8) | fg(16)

def display_color_8():
    light_cube = []
    dark_cube = []
    for i in range(16, 232):
        if ((i+2) % 36) < 18:
            light_cube.append(
                cell_black_text(i)
            )
        else:
            dark_cube.append(
                cell_white_text(i)
            )
    return vbox([
        *(hbox(row) for row in batched(dark_cube, 6*3)),
        *(hbox(row) for row in batched(light_cube, 6*3)),
    ])


def title(c: Layout):
    return static_box([
        hbar,
        c | bold | offset(x=1) | shrink,
    ]) | custom_padding(top = 1, bottom = 0)

def flex_item(string: str):
    return text(string) | center | custom_padding(1, 1, 1, 1)
def flex_itemw(string: str):
    return text(string) | center | custom_padding(1, 1, 1, 1)

layout = vbox([
    hbox_flex([
        vbox([
            text("4 bit") | fg(Color4.GREEN),
            text("8 bit") | fg(45),
            text("True\ncolor") | fg(Color4.BRIGHT_RED),

        ]) | padding | flex,
        display_color_8()
    ]) | border_with_title(text("Colors")),
    display_char_styles() | border_with_title(text("Styles")),
    hbox_flex([
        vbox([
            hbox_flex(intersperse([
                flex_item("flex") | flex,
                flex_item("flex_custom(2)") | flex_custom(2),
                flex_item("no flex"),
            ], sep=vbar)) | border_with_title(text("Flexible Containers")),
            adaptive_text(LOREM) | padding | dim | border_with_title(text("Text wrapping")),
        ]) | flex,
        hbox_flex_wrap(
            [(text("flex") | custom_padding(1, 1, 1, 1) | styled(border, rule_fg(i+0)) | flex_custom(1, basis=True)) for i in range(10)]
        )| border_with_title(text("Flexible and Wrappable Containers")) | flex,
    ]),
])

result = layout_to_result(layout, Rect(102, 45))
print(result_to_str(result))

Color Capabilities Test

from functui.common import *
from functui.classes import *
from functui.rich_text import adaptive_text, span
from functui import Rect, layout_to_result, result_to_str, Color4
from itertools import batched

def cell_white_text(color8: int):
    return text(f"{color8: >3}") | padding | bg_fill | bg(color8) | fg(Color4.BRIGHT_WHITE)
def cell_black_text(color8: int):
    return text(f"{color8: >3}") | padding | bg_fill | bg(color8) | fg(16)

def display_color_8():
    regular = []
    intense = []
    light_cube = []
    dark_cube = []
    blacks = []
    whites = []
    for i in range(8):
        regular.append(
            cell_white_text(i)
        )
        intense.append(
            cell_black_text(i+8)
        )
    for i in range(16, 232):
        if ((i+2) % 36) < 18:
            light_cube.append(
                cell_black_text(i)
            )
        else:
            dark_cube.append(
                cell_white_text(i)
            )
    for i in range(232, 244):
        blacks.append(
            cell_white_text(i)
        )
        whites.append(
            cell_black_text(i+12)
        )
    return vbox([
        text("Regular and bright colors are rendered differently depending on terminal theme"),
        hbox([text("Regular:  "), *regular]),
        hbox([text("Bright:   "), *intense]),
        text(" "),
        text("Color Cube"),
        *(hbox(row) for row in batched(dark_cube, 6*3)),
        *(hbox(row) for row in batched(light_cube, 6*3)),
        text(" "),
        text("Color ramps, black and white intentionaly excluded"),
        hbox(blacks),
        hbox(whites),
    ])

layout = vbox([
    display_color_8() | shrink,
])
result = layout_to_result(layout, Rect(140, 40))

if __name__ == "__main__":
    print(result_to_str(result))


Interactive examples using the elm architecture

Template for ELM projects using raw IO

from functui.classes import *
from functui.common import *
from functui.nav import ROOT_HORIZONTAL, ROOT_VERTICAL, InteractibleID, NavState, DEFAULT_NAV_BINDINGS, interaction_area
from functui.io.raw import terminal

from dataclasses import dataclass


@dataclass
class Model():
    nav: NavState

    # Add more attributes to contain all of your persistent state.


def update(input: InputEvent, res: Result, m: Model):
    # update NavState for keyboard and mouse interactivity.
    action = None
    if input.key_event in DEFAULT_NAV_BINDINGS:
        action = DEFAULT_NAV_BINDINGS[input.key_event]

    m.nav = m.nav.update(
        res=res,
        action=action, 
        nav_tree=[],
        mouse_position=input.mouse_position_event
    )

    # Put your update code here.

    # Create your keyboard navigation tree here.


def view(m: Model):
    # Layout rendering code here.
    layout = vbox([
        text("Hello World!") | border,
        text("Press ctrl+c to exit.") | fg(Color4.CYAN) | border,
    ])
    return layout


m = Model(
    nav = NavState(),
)


with terminal() as term:
    while True:
        # render
        res = layout_to_result(view(m), term.get_terminal_size())
        term.display_result(res)

        # wait for input
        event = term.block_until_input()

        # update
        if event.key_event == "ctrl+c":
            break
        update(event, res, m)

ELM Counter App with ansi IO

from dataclasses import dataclass
from functui.common import *
from functui import layout_to_str, Rect, Layout

import os

# To avoid writing boilerplate use a dataclass that is included
# in pythons standard library.
@dataclass
class Model():
    counter: int = 0

def update(input: str, m: Model):
    if input == "i":
        m.counter += 1
    elif input == "d":
        m.counter -= 1

def view(m: Model) -> Layout:
    layout = text(str(m.counter)) | center | border
    return layout

m = Model()

# Event loop.
# this example uses the build in input functions instead of io.raw
while True:
    # first clear terminal and draw
    os.system('cls' if os.name == 'nt' else 'clear')
    print(layout_to_str(layout=view(m), dimensions=Rect(11, 3)))

    # then get input
    i = input()
    if i == "exit":
        break # exit program

    # then update
    update(i, m)

Multiple interactibles with ELM

from functui.classes import *
from functui.common import *
from functui.nav import ROOT_HORIZONTAL, ROOT_VERTICAL, InteractibleID, NavState, DEFAULT_NAV_BINDINGS, interaction_area
from functui.io.raw import terminal

from dataclasses import dataclass


@dataclass
class Model():
    nav: NavState
    button_1: InteractibleID
    button_2: InteractibleID


def update(input: InputEvent, res: Result, m: Model):
    action = None
    if input.key_event in DEFAULT_NAV_BINDINGS:
        action = DEFAULT_NAV_BINDINGS[input.key_event]

    m.nav = m.nav.update(
        res=res,
        action=action, 
        nav_tree=[m.button_1, m.button_2],
        mouse_position=input.mouse_position_event
    )


# If you want ui nodes to be reusable, put them into a function.
def selectable_element(nav: NavState, id: InteractibleID, string: str):
    return text(string) | styled(
        border,
        (rule_fg(Color4.BLUE) if m.nav.is_hover(id) else StyleRule()) | (rule_bg(Color4.BLUE) if m.nav.is_active(id) else StyleRule())
    ) | interaction_area(id)



def view(m: Model):
    layout = vbox([
        selectable_element(m.nav, m.button_1, "Hello"),
        selectable_element(m.nav, m.button_2, "World"),
    ]) | border
    return layout


root = ROOT_VERTICAL

# for interactible ids that never change it is possible to define them when model is first created.
m = Model(
    nav = NavState(),
    button_1 = root.child(0),
    button_2 = root.child(1),
)


with terminal() as term:
    while True:
        # render
        res = layout_to_result(view(m), term.get_terminal_size())
        term.display_result(res)

        # wait for input
        event = term.block_until_input()

        # update
        if event.key_event == "ctrl+c":
            break
        update(event, res, m)

Interactive examples with elm and curses

Template for ELM projects using curses IO

from functui.classes import *
from functui.common import *
from functui.nav import ROOT_HORIZONTAL, ROOT_VERTICAL, InteractibleID, NavState, default_nav_bindings, interaction_area
from functui.io.curses import get_input_event, draw_result, wrapper

import curses
from dataclasses import dataclass


@dataclass
class Model():
    nav: NavState

    # Add more attributes to contain all of your persistent state.


def update(input: InputEvent, res: Result, m: Model):
    # update NavState for keyboard and mouse interactivity.
    action = None
    if input.key_event in default_nav_bindings:
        action = default_nav_bindings[input.key_event]

    m.nav = m.nav.update(
        res=res,
        action=action, 
        nav_tree=[],
        mouse_position=input.mouse_position_event
    )

    # Put your update code here.

    # Create your keyboard navigation tree here.


def view(m: Model):
    # Layout rendering code here.
    layout = vbox([
        text("Hello World!") | border,
        text("Press ctrl+c to exit.") | fg(Color4.CYAN) | border,
    ])
    return layout


m = Model(
    nav = NavState(),
)


def main(stdscr: curses.window):
    while True:
        y, x = stdscr.getmaxyx()
        res = layout_to_result(view(m), Rect(x, y))
        draw_result(res, stdscr)

        key: InputEvent = get_input_event(stdscr)
        if key.key_event == 'ctrl+c':
            break # exit program
        update(key, res, m)

if __name__ == "__main__":
    wrapper(main)

Print Input Event with ELM and curses

from functui.classes import *
from functui.common import *
from functui.flex import hbox_flex, flex
from functui.nav import ROOT_HORIZONTAL, ROOT_VERTICAL, InteractibleID, NavState, DEFAULT_NAV_BINDINGS, interaction_area
from functui.io.curses import get_input_event, draw_result, wrapper

import curses
from dataclasses import dataclass, field



@dataclass
class Model():
    nav: NavState
    keycodes: list[str] = field(default_factory=list)
    mouse_positions: list[Coordinate] = field(default_factory=list)


def update(input: InputEvent, res: Result, m: Model):
    action = None
    if input.key_event in DEFAULT_NAV_BINDINGS:
        action = DEFAULT_NAV_BINDINGS[input.key_event]

    m.nav = m.nav.update(
        res=res,
        action=action, 
        nav_tree=[],
        mouse_position=input.mouse_position_event
    )

    if input.key_event is not None:
        m.keycodes.append(input.key_event)
    if input.mouse_position_event is not None:
        m.mouse_positions.append(input.mouse_position_event)

    if len(m.keycodes) > 50:
        del m.keycodes[0]
    if len(m.mouse_positions) > 50:
        del m.mouse_positions[0]

def view(m: Model):
    layout = hbox_flex([
        vbox(
            [text(f"<{i}>") | padding for i in m.keycodes],
            reverse=True
        ) | border_with_title(text("[key event]") | center) | flex,
        vbox(
            [text(f"<{repr(i)}>") | padding for i in m.mouse_positions],
            reverse=True
        ) | border_with_title(text("[mouse position event]") | center)| flex,
    ]) | padding
    return layout


m = Model(
    nav = NavState(),
)


def main(stdscr: curses.window):
    while True:
        y, x = stdscr.getmaxyx()
        res = layout_to_result(view(m), Rect(x, y))
        draw_result(res, stdscr)

        key: InputEvent = get_input_event(stdscr)
        if key.key_event == 'ctrl+c':
            break # exit program
        update(key, res, m)

if __name__ == "__main__":
    wrapper(main)