A layered terminal compositor library for C99 and POSIX.1-2008. Ships a binary called tui and a static archive called liblapka.a. No dependencies. No comments in the code.
- Fourteen verbs, called from bash as
tui <verb> .... Each prints a result on stdout and exits with a code bash cancaseon. - Nine primitives, linked into your own program as
liblapka.a.LapkaList,LapkaText,LapkaTabs,LapkaProgress,LapkaSummary,LapkaTree,LapkaNotification,LapkaPager,LapkaEditor. - A layer compositor with
OPAQUEandTRANSPARENTblending, z-ordering, and input routing across layers. - UTF-8 everywhere. Wide characters, combining marks, CJK, emoji. Codepoint-based cursor arithmetic.
- Damage-diffed rendering. No flicker. No
\033[2J. - Signal-safe teardown. The terminal is restored on every exit path, including
SIGSEGV.
make
make testProduces liblapka.a and tui.
Every verb takes a title, a body, and its arguments. Results come back on stdout.
# Message. Waits for Enter before returning.
tui msg "Notice" "The disk will be erased."
# Yes / no
if tui yesno "Confirm" "Erase /dev/sda?"; then
wipefs -a /dev/sda
fi
# One-of-N
disk=$(tui menu "Disk" "Choose target drive:" /dev/sda /dev/nvme0n1 /dev/sdb)
# Multi-select with space to toggle
groups=$(tui checklist "Groups" "Pick groups for artix:" \
wheel audio video storage lp network)
# Fuzzy multi-select from stdin
packages=$(printf '%s\n' git vim neovim tmux firefox \
| tui filter "Packages" "Type to search:" --multi)
# Free text with a default
hostname=$(tui input "Hostname" "Enter system hostname:" --default artix)
# Masked input. Must be captured; stdout cannot be a tty.
password=$(tui password "Root" "Enter root password:") || exit 1
# Confirmed password. Retries up to 3 times by default.
password=$(tui password-confirm "Root" "Enter root password:" --tries 3)
# Long-running command with a spinner and rolling output
tui spin "Installing" "Base system" -- pacstrap /mnt base linux linux-firmware
# Pager with search
tui show-file "Install log" /tmp/install.log
# Full-screen editor
tui editor /etc/hostname
# File and directory browser
path=$(tui explorer /etc) || exit 1
# Non-blocking toast in the bottom-right corner
tui toast "Config saved" --duration=2000
# Hub. Form engine driven by a text file.
tui hub --in=/tmp/hub-in --out=/tmp/hub-out </dev/tty || exit 1Every verb accepts --theme=FILE, overriding LAPKA_THEME. spin also accepts --where=TL|TC|TR|LC|CC|RC|BL|BC|BR and --hold N|enter|none.
By default each verb enters and leaves the alt screen itself. To run several verbs back-to-back with no screen toggle between them, hold the alt screen from bash:
tui_session_begin() {
printf '\033[?1049h\033[H\033[2J' > /dev/tty
export LAPKA_ALT_SCREEN=1
}
tui_session_end() {
printf '\033[?1049l' > /dev/tty
unset LAPKA_ALT_SCREEN
}
trap tui_session_end EXIT INT TERM
tui_session_begin
disk=$(tui menu "Disk" "Pick:" /dev/sda /dev/nvme0n1)
hostname=$(tui input "Hostname" "Enter:")
tui yesno "Confirm" "Proceed?"
tui_session_end#include <lapka/lapka.h>
#include <stdio.h>
typedef struct {
LapkaPrimitive *list;
int exit_code;
} App;
static void app_draw(void *ud, LapkaFramebuffer *fb) {
App *a = ud;
const LapkaTheme *theme = lapka_theme_active();
LapkaRect title_r = { 0, 0, fb->w, 1 };
lapka_draw_text(fb, title_r, "-- Pick one --", theme->title, 0, LAPKA_ATTR_BOLD);
if (fb->h > 2) {
LapkaCell *sub = fb->cur + (size_t)fb->w * 2;
a->list->draw(a->list, sub, fb->w, fb->h - 2, theme);
}
}
static bool app_handle(void *ud, const LapkaEvent *ev) {
App *a = ud;
if (ev->type == LAPKA_EVENT_KEY) {
if (ev->key.code == LAPKA_KEY_ENTER) { a->exit_code = 0; return true; }
if (ev->key.code == LAPKA_KEY_ESC) { a->exit_code = 1; return true; }
}
a->list->handle(a->list, ev);
return false;
}
int main(void) {
const char *items[] = { "alpha", "beta", "gamma" };
App a = { .list = lapka_list_new(items, 3, LAPKA_SELECT_ONE), .exit_code = 2 };
if (!a.list) return 2;
LapkaInitOptions opts = { 1, 1, 1, 0 };
LapkaRunCallbacks cb = { app_draw, app_handle, NULL, &a };
int rc = lapka_run(&opts, &cb, -1);
if (rc == 0 && a.exit_code == 0) {
printf("%s\n", lapka_list_cursor_item(a.list));
}
lapka_primitive_free(a.list);
return rc != 0 ? rc : a.exit_code;
}lapka_draw_text is declared in src/render/draw.h, which is not installed. To build this example against the current tree, copy src/render/draw.h into your include path or include the source.
Build with:
cc -std=c99 -D_POSIX_C_SOURCE=200809L -O2 myapp.c liblapka.a -o myapptui hub renders a form described by a text file that bash generates. There is no compiled-in form table.
cat > /tmp/hub-in <<'EOF'
hub Disk Configuration
action Back
action Proceed
category target "Target"
item key=DISK label="Disk" widget=menu choices_cmd="lsblk -dpno NAME,SIZE,MODEL -e 7" value=
item key=FS_TYPE label="Filesystem" widget=menu value=ext4
choice ext4
choice btrfs
choice xfs
choice f2fs
item key=HOSTNAME label="Hostname" widget=input value=artix
category boot "Boot"
item key=USE_LUKS label="Encrypt root" widget=yesno value=no
item key=LUKS_PASS label="Passphrase" widget=password visible_if="USE_LUKS=yes"
EOF
touch /tmp/hub-out
tui hub --in=/tmp/hub-in --out=/tmp/hub-out </dev/tty || exit 1
while IFS='=' read -r k v; do state_set "$k" "$v"; done < /tmp/hub-outValues come back as flat KEY=value lines for state_set. Items with visible_if are hidden unless their condition matches; hidden items have their values cleared automatically.
Items can also point at another hub file:
item key=MANAGE label="Users" widget=subhub subhub="users.hub"
users.hub is a full hub in its own right. It opens as a full-screen layer on top of the parent. On Proceed, its values merge into the parent's; on Esc, they are discarded. A relative subhub path resolves against the directory of the IN file that declared it.
--check makes the hub refuse to open --in or --out unless both are root-owned regular files with mode 0600 or stricter. Use it when the state contains passwords.
Every verb accepts --theme=FILE, and reads LAPKA_THEME from the environment if the flag is absent. The theme file is KEY=value, one per line. Values are 0xRRGGBB for colors, plain strings for glyphs, enum names for borders.
bg=0x000000
fg=0xFFFFFF
accent=0x00AF00
title=0xFF87D7
muted=0x888888
error=0xCC0000
warning=0xCCCC00
success=0x00AF00
modal_bg=0x000000
modal_border=0x9F5A85
modal_title=0xC06BA5
border=single
cursor=|
check=x
radio_on=(*)
radio_off=( )
bullet=*
arrow=>
Unknown keys are ignored. Missing keys keep the default. If modal_bg, modal_border, or modal_title are not set, they are derived from bg and title.
Every verb uses one placement vocabulary. Nine anchors, four size modes, four border styles, padding, margins.
LapkaBox box = {
.anchor = LAPKA_ANCHOR_CC,
.width = { LAPKA_SIZE_PERCENT, 60 },
.height = { LAPKA_SIZE_FIT, 0 },
.border = LAPKA_BORDER_ROUNDED,
.pad = { 1, 2, 1, 2 },
};
LapkaRect r = lapka_place(screen, box, natural_w, natural_h);Layers compose with OPAQUE (solid panels, modals, editors) and TRANSPARENT (toasts, floating hints). No dimming; a modal stands out by drawing a solid rectangle of a different color on top.
docs/spec.md— the specification.docs/headers.md— every public symbol grouped by header.docs/TODO.md— deferred work.docs/CONTRIBUTORS.md— contributions under alternate terms.CONTRIBUTING.md— how to contribute.
CLEAR License v1. Permissive, not compatible with copyleft. See LICENSE.
Linking liblapka.a or liblapka.so into a program licensed under the GPL, LGPL, AGPL, or a compatible copyleft license may constitute a Combined Work under Section 11 and is not permitted without separate written permission. Invoking tui as a subprocess from a shell script is not a Combined Work.