
Opinionated terminal UI framework with component-builder architecture, navigation stack, reactive state, built-in views (lists, tables, editors, paginated/tree), framework overlays (help, toasts, dialogs), keyboard shortcuts and theming.
An opinionated TUI application framework for Kotlin Multiplatform Native, built on top of ftxui-kt. It provides a component builder architecture, a navigation stack, built-in views, and framework-level overlays (help, toasts, dialogs, logging, preferences) so you can focus on writing application logic rather than plumbing.
Targets: macOS ARM64, Linux x64
Kotlin: 2.3.21
The framework follows a component builder pattern driven by AppContext:
runApp(name, builder)
↓ creates
AppContext (preferences, navigator, requestRedraw, post, terminalSize)
↓ passed to
Component builders (plain classes with a build() method)
↓ register shortcuts via
navigator.registerShortcutsForComponent(comp, shortcuts)
↑ trigger redraw on state change
AppContext.requestRedraw()
Screens are plain classes that take a Navigator and AppContext as constructor parameters. The build() method calls context.run { ... } to access the AppContext view builder extensions in scope:
class MyScreen(private val navigator: Navigator, private val context: AppContext) {
private var count by context.mutableStateOf(0)
fun build(): Component = context.run {
val comp = list(
getEntries = { getItems(count) },
renderItem = { item, focused ->
if (focused) text(item.name).inverted() else text(item.name)
},
renderHeader = { name -> hbox(text("── $name ──").bold(), filler()) }
)
navigator.registerShortcutsForComponent(comp, listOf(
Shortcut(Key.CtrlS, "^S Save", description = "Save changes") { saveData() },
))
comp
}
}Reactivity can be managed in two ways:
Local State (mutableStateOf): A Compose-like delegate available on AppContext. Mutating a variable defined with mutableStateOf automatically schedules a redraw.
var selectedIndex by context.mutableStateOf(0)Flow / StateFlow: Launch a coroutine to collect state and call requestRedraw(). Use navigator.scopeFor(comp) to get a scope that is automatically cancelled when the component is popped from the stack.
fun build(): Component = context.run {
val comp = list(getEntries = { viewModel.state.value.items }, ...)
navigator.scopeFor(comp).launch {
viewModel.state.collect { context.requestRedraw() }
}
comp
}Views are declarative Component functions built as extensions on AppContext (e.g. list, table, split). They wrap native FTXUI focus and event handling, and accept lambda retrievers (e.g., getEntries = { ... }) to pull state dynamically on render.
Navigator is available inside AppContext via context.navigator and can be stored as a constructor parameter. Use it to push/pop screens or show global UI overlays:
navigator.push(DetailScreen(navigator, context, item).build())
navigator.pop()
navigator.showDialog(Dialog.Alert(title = "Done", message = "Saved."))
navigator.notify("Item saved", Toast.SHORT, Toast.Type.Success)Additional Navigator APIs:
// Coroutine scope tied to this component's lifetime on the stack
val scope = navigator.scopeFor(comp)
// Callback invoked when the component becomes/stops being the top screen
navigator.onTopChanged(comp) { isVisible -> /* pause/resume work */ }
// Register shortcuts shown in the status bar and ? help overlay
navigator.registerShortcutsForComponent(comp, listOf(...))
// Register shortcuts lazily (re-evaluated each time they're needed)
navigator.registerShortcutsForComponent(comp) { buildDynamicShortcuts() }fun main() {
runApp(
name = "my-app",
initialComponentBuilder = { MyScreen(navigator, this).build() },
confirmOnQuit = true,
enableCtrlZ = true,
)
}runApp creates the AppContext and Preferences automatically. The builder lambda receives AppContext as this; navigator is available on it.
All views are instantiated via AppContext extension methods and return a standard FTXUI Component. Call them inside a context.run { ... } block.
Scrollable list with headers, fuzzy search, and vim-style navigation.
val comp = list(
getEntries = {
buildList {
add(ListEntry.Header("Fruits"))
fruits.forEach { add(ListEntry.Item(it) { navigator.push(DetailScreen(...).build()) }) }
}
},
renderItem = { fruit, focused ->
if (focused) text(fruit.name).inverted() else text(fruit.name)
},
renderHeader = { name -> hbox(text("── $name ──").bold(), filler()) },
toSearchString = { it.name },
style = ListStyle(focusedItemBackground = Color.Blue),
keybindings = ListKeybindings(), // optional, fully customisable
)Keys: j/k navigate, g/G top/bottom, Ctrl+U/D half-page, / fuzzy search.
Sortable table with customisable column renderers.
val comp = table(
getRows = { FRUIT_ROWS },
columns = listOf(
TableColumn("Name", extract = { it.name }),
TableColumn(
header = "Category",
extract = { it.category },
renderCell = { item, width, focused ->
val color = if (item.category == "Fruit") Color.Green else Color.Yellow
val el = text(item.category.padEnd(width + 3)).color(color)
if (focused) el.inverted() else el
}
),
),
onEnter = { row -> navigator.push(DetailScreen(...).build()) }
)Keys: j/k navigate, s cycles column sort (▲ / ▼ / off), Enter triggers onEnter.
Read-only scrollable text panel with incremental search.
val comp = pager(
getState = { PagerState(lines = myLines, showLineNumbers = true) }
)Keys: j/k/g/G scroll, / search, n/N next/prev match.
Hierarchical tree with expand/collapse nodes.
val comp = tree(
getState = { TreeState(roots) },
renderNode = { label, depth, focused, hasChildren, isExpanded ->
if (focused) text(label).inverted() else text(label)
}
)Create a tree of TreeNode<T> which defines children, isExpanded, and optional onToggle/onEnter callbacks.
Keys: j/k navigate, →/l expand, ←/h collapse, Enter triggers onEnter.
Two components side-by-side; Tab/Shift+Tab switches focus, and the inactive panel is dimmed automatically.
val comp = split(
left = leftComponent,
right = rightComponent,
leftTitle = "Left Pane",
rightTitle = "Right Pane"
)Multiline text editor with built-in undo/redo stack. Requires enableCtrlZ = true in runApp().
var content = "Initial Text"
val comp = textEditor(
content = ::content,
showLineNumbers = true,
onContentChange = { newText -> Logger.debug("Text length: ${newText.length}") },
onStateChange = { newState ->
editorState = newState
context.requestRedraw()
}
)Keys: Arrow keys, Home/End, Page Up/Down, Backspace/Delete/Enter, Ctrl+Z/Y undo/redo.
POSIX filesystem browser.
val comp = filePicker(
initialPath = ".",
onFileSelected = { path -> navigator.pop(); handleFile(path) },
showHiddenInitially = false,
filter = { entry -> entry.name.endsWith(".kt") }
)Keys: j/k navigate, Enter enter directory or select file, Backspace/h go up, / filter, . toggle hidden files.
Grid of independent cells, each rendering a child Component.
val comp = dashboard(
columns = 2,
cells = listOf(
DashboardCell("CPU Gauge", render = { cpuGaugeComponent }),
DashboardCell("Logs", render = { logListComponent })
)
)Keys: Tab/Shift+Tab cycle active cell focus.
Lazily loads pages of data reactively via a suspend loader.
val comp = paginatedList(
pageSize = 50,
loadThreshold = 10,
loadPage = { offset, limit -> fetchItems(offset, limit) },
renderItem = { item, focused -> if (focused) text(item.name).inverted() else text(item.name) },
renderHeader = { name -> hbox(text(" $name").bold(), filler()) }
)A loading row appears at the bottom while the next page fetches. Fuzzy search / filters over all currently loaded items.
Renders a pipeline of steps with status icons, spinners, and expandable output.
val comp = stepProgress(
getState = {
StepProgressState(
steps = listOf(
ProgressStep("Build", StepStatus.Done, output = buildLogs),
ProgressStep("Test", StepStatus.Running)
),
spinnerTick = currentTick
)
}
)Keys: j/k navigate, →/l expand output, ←/h collapse, Space toggle.
Each built-in view can be styled individually by passing a specific *Style config class. By default, styles fall back to the active theme's colors defined in Theme.current.
val comp = list(
getEntries = { entries },
renderItem = { item, focused -> text(item) },
renderHeader = { text(it) },
style = ListStyle(
focusedItemForeground = Color.Black,
focusedItemBackground = Color.Yellow,
headerForeground = Color.Cyan
)
)ListStyle: focusedItemForeground, focusedItemBackground, headerForeground, scrollThumb, searchHighlight
TableStyle: headerForeground, focusedRowForeground, focusedRowBackground, sortIndicatorColor, scrollThumb, borderStyle
TreeStyle: focusedNodeForeground, focusedNodeBackground, expandedIcon, collapsedIcon, leafIndent, scrollThumb
SplitStyle: activeTitleForeground, inactiveTitleForeground, borderStyle, activeBorderStyle
DashboardStyle: focusedTitleForeground, unfocusedTitleForeground, borderStyle, focusedBorderStyle
PagerStyle: searchHighlight, lineNumberColor, scrollThumb
StepProgressStyle: pendingColor, runningColor, doneColor, failedColor, skippedColor
FilePickerStyle: directoryColor, fileColor, pathColor, scrollThumb
TextEditorStyle: lineNumbersColor, cursorForeground, cursorBackground, scrollThumb
To configure global colors, subclass or instantiate ThemeColors and assign it:
Theme.current = ThemeColors(
accent = Color.Green,
border = Color.GrayDark
)Every built-in view accepts an optional *Keybindings parameter that lets you remap any key without subclassing:
val comp = list(
getEntries = { entries },
renderItem = { item, focused -> text(item) },
renderHeader = { text(it) },
keybindings = ListKeybindings(
moveUpChars = listOf("k", "p"),
moveDownChars = listOf("j", "n"),
)
)Available keybinding types: ListKeybindings, TableKeybindings, TreeKeybindings, PagerKeybindings, StepProgressKeybindings, FilePickerKeybindings, DashboardKeybindings, SplitKeybindings, TextEditorKeybindings.
| Shortcut | Overlay |
|---|---|
? |
Help — all registered shortcuts + framework defaults |
Ctrl+P |
Command palette — fuzzy search over registered shortcuts |
Ctrl+N |
Notification history — scroll with j/k, Esc to close |
Ctrl+L |
Log viewer — scroll with j/k/g/G, Esc to close |
Ctrl+Alt+P |
Performance overlay — FPS, frame time, stack depth, terminal size |
navigator.showDialog(Dialog.Alert(title = "Info", message = "Done."))
navigator.showDialog(Dialog.Confirm(
title = "Delete?",
message = "This cannot be undone.",
onConfirm = { doDelete() },
onCancel = { /* nothing */ },
))
navigator.showDialog(Dialog.Prompt(
title = "Enter name",
placeholder = "Name",
onSubmit = { name -> save(name) },
))navigator.notify("Saved", Toast.SHORT, Toast.Type.Success)
navigator.notify("Low disk space", Toast.SHORT, Toast.Type.Warning)
navigator.notify("Load failed", Toast.LONG, Toast.Type.Error)
navigator.notify("FYI", Toast.SHORT, Toast.Type.Info) // defaultUp to 3 toasts are stacked simultaneously. Each shows an animated countdown border. All toasts are accessible via Ctrl+N notification history.
Logger.debug("Verbose detail")
Logger.info("User opened settings")
Logger.warn("Config missing, using defaults")
Logger.error("Connection refused")Logs are kept in a 1000-entry in-memory ring buffer, rendered inside a log panel overlay (Ctrl+L).
Persistent key-value store backed by ~/.config/<appName>/prefs.properties. The Preferences instance is created automatically by runApp and is accessible via AppContext.preferences.
val count = preferences.getInt("launch.count", default = 0)
preferences.setInt("launch.count", count + 1)
preferences.setString("last.file", "/path/to/file")
val path = preferences.getString("last.file", default = "")
preferences.setBoolean("dark.mode", true)Preferences are saved automatically on app exit.
Generic undo/redo stack (used internally by textEditor).
val history = UndoRedoStack(initial = emptyList<String>(), maxSize = 100)
history.push(newState)
history.undo()
history.redo()
history.reset(newInitial)Switches between two layouts based on terminal width or height. Both variants are called as extensions on AppContext.
// Switch on terminal width
val comp = responsiveHorizontal(
breakpoint = 120,
narrow = { buildNarrowLayout() },
wide = { buildWideLayout() },
)
// Switch on terminal height
val comp = responsiveVertical(
breakpoint = 30,
short = { buildShortLayout() },
tall = { buildTallLayout() },
)Viewport measures the actual pixel-dimensions of the slot assigned to a component after layout. It is created via AppContext.viewport() and used internally by scrollable views; it can also be used in custom renderers when you need the true available height or width.
val vp = viewport()
// vp.width / vp.height — exact slot size after the first render frame# Build (macOS ARM64)
./gradlew linkDebugExecutableMacosArm64
# Run the demo app
./build/bin/macosArm64/debugExecutable/ftxui-kt-framework.kexe
# Build (Linux x64)
./gradlew linkDebugExecutableLinuxX64Ctrl+N, Ctrl+L, Ctrl+P, and ? are reserved by the framework; do not register them on your components.enableCtrlZ = true is required in runApp() to use undo/redo in textEditor.navigator.scopeFor(comp) rather than GlobalScope for coroutines that collect ViewModel state — the scope is cancelled automatically when the component is popped.navigator.onTopChanged(comp) to pause background work (polling, refresh loops) while a screen is obscured by another.An opinionated TUI application framework for Kotlin Multiplatform Native, built on top of ftxui-kt. It provides a component builder architecture, a navigation stack, built-in views, and framework-level overlays (help, toasts, dialogs, logging, preferences) so you can focus on writing application logic rather than plumbing.
Targets: macOS ARM64, Linux x64
Kotlin: 2.3.21
The framework follows a component builder pattern driven by AppContext:
runApp(name, builder)
↓ creates
AppContext (preferences, navigator, requestRedraw, post, terminalSize)
↓ passed to
Component builders (plain classes with a build() method)
↓ register shortcuts via
navigator.registerShortcutsForComponent(comp, shortcuts)
↑ trigger redraw on state change
AppContext.requestRedraw()
Screens are plain classes that take a Navigator and AppContext as constructor parameters. The build() method calls context.run { ... } to access the AppContext view builder extensions in scope:
class MyScreen(private val navigator: Navigator, private val context: AppContext) {
private var count by context.mutableStateOf(0)
fun build(): Component = context.run {
val comp = list(
getEntries = { getItems(count) },
renderItem = { item, focused ->
if (focused) text(item.name).inverted() else text(item.name)
},
renderHeader = { name -> hbox(text("── $name ──").bold(), filler()) }
)
navigator.registerShortcutsForComponent(comp, listOf(
Shortcut(Key.CtrlS, "^S Save", description = "Save changes") { saveData() },
))
comp
}
}Reactivity can be managed in two ways:
Local State (mutableStateOf): A Compose-like delegate available on AppContext. Mutating a variable defined with mutableStateOf automatically schedules a redraw.
var selectedIndex by context.mutableStateOf(0)Flow / StateFlow: Launch a coroutine to collect state and call requestRedraw(). Use navigator.scopeFor(comp) to get a scope that is automatically cancelled when the component is popped from the stack.
fun build(): Component = context.run {
val comp = list(getEntries = { viewModel.state.value.items }, ...)
navigator.scopeFor(comp).launch {
viewModel.state.collect { context.requestRedraw() }
}
comp
}Views are declarative Component functions built as extensions on AppContext (e.g. list, table, split). They wrap native FTXUI focus and event handling, and accept lambda retrievers (e.g., getEntries = { ... }) to pull state dynamically on render.
Navigator is available inside AppContext via context.navigator and can be stored as a constructor parameter. Use it to push/pop screens or show global UI overlays:
navigator.push(DetailScreen(navigator, context, item).build())
navigator.pop()
navigator.showDialog(Dialog.Alert(title = "Done", message = "Saved."))
navigator.notify("Item saved", Toast.SHORT, Toast.Type.Success)Additional Navigator APIs:
// Coroutine scope tied to this component's lifetime on the stack
val scope = navigator.scopeFor(comp)
// Callback invoked when the component becomes/stops being the top screen
navigator.onTopChanged(comp) { isVisible -> /* pause/resume work */ }
// Register shortcuts shown in the status bar and ? help overlay
navigator.registerShortcutsForComponent(comp, listOf(...))
// Register shortcuts lazily (re-evaluated each time they're needed)
navigator.registerShortcutsForComponent(comp) { buildDynamicShortcuts() }fun main() {
runApp(
name = "my-app",
initialComponentBuilder = { MyScreen(navigator, this).build() },
confirmOnQuit = true,
enableCtrlZ = true,
)
}runApp creates the AppContext and Preferences automatically. The builder lambda receives AppContext as this; navigator is available on it.
All views are instantiated via AppContext extension methods and return a standard FTXUI Component. Call them inside a context.run { ... } block.
Scrollable list with headers, fuzzy search, and vim-style navigation.
val comp = list(
getEntries = {
buildList {
add(ListEntry.Header("Fruits"))
fruits.forEach { add(ListEntry.Item(it) { navigator.push(DetailScreen(...).build()) }) }
}
},
renderItem = { fruit, focused ->
if (focused) text(fruit.name).inverted() else text(fruit.name)
},
renderHeader = { name -> hbox(text("── $name ──").bold(), filler()) },
toSearchString = { it.name },
style = ListStyle(focusedItemBackground = Color.Blue),
keybindings = ListKeybindings(), // optional, fully customisable
)Keys: j/k navigate, g/G top/bottom, Ctrl+U/D half-page, / fuzzy search.
Sortable table with customisable column renderers.
val comp = table(
getRows = { FRUIT_ROWS },
columns = listOf(
TableColumn("Name", extract = { it.name }),
TableColumn(
header = "Category",
extract = { it.category },
renderCell = { item, width, focused ->
val color = if (item.category == "Fruit") Color.Green else Color.Yellow
val el = text(item.category.padEnd(width + 3)).color(color)
if (focused) el.inverted() else el
}
),
),
onEnter = { row -> navigator.push(DetailScreen(...).build()) }
)Keys: j/k navigate, s cycles column sort (▲ / ▼ / off), Enter triggers onEnter.
Read-only scrollable text panel with incremental search.
val comp = pager(
getState = { PagerState(lines = myLines, showLineNumbers = true) }
)Keys: j/k/g/G scroll, / search, n/N next/prev match.
Hierarchical tree with expand/collapse nodes.
val comp = tree(
getState = { TreeState(roots) },
renderNode = { label, depth, focused, hasChildren, isExpanded ->
if (focused) text(label).inverted() else text(label)
}
)Create a tree of TreeNode<T> which defines children, isExpanded, and optional onToggle/onEnter callbacks.
Keys: j/k navigate, →/l expand, ←/h collapse, Enter triggers onEnter.
Two components side-by-side; Tab/Shift+Tab switches focus, and the inactive panel is dimmed automatically.
val comp = split(
left = leftComponent,
right = rightComponent,
leftTitle = "Left Pane",
rightTitle = "Right Pane"
)Multiline text editor with built-in undo/redo stack. Requires enableCtrlZ = true in runApp().
var content = "Initial Text"
val comp = textEditor(
content = ::content,
showLineNumbers = true,
onContentChange = { newText -> Logger.debug("Text length: ${newText.length}") },
onStateChange = { newState ->
editorState = newState
context.requestRedraw()
}
)Keys: Arrow keys, Home/End, Page Up/Down, Backspace/Delete/Enter, Ctrl+Z/Y undo/redo.
POSIX filesystem browser.
val comp = filePicker(
initialPath = ".",
onFileSelected = { path -> navigator.pop(); handleFile(path) },
showHiddenInitially = false,
filter = { entry -> entry.name.endsWith(".kt") }
)Keys: j/k navigate, Enter enter directory or select file, Backspace/h go up, / filter, . toggle hidden files.
Grid of independent cells, each rendering a child Component.
val comp = dashboard(
columns = 2,
cells = listOf(
DashboardCell("CPU Gauge", render = { cpuGaugeComponent }),
DashboardCell("Logs", render = { logListComponent })
)
)Keys: Tab/Shift+Tab cycle active cell focus.
Lazily loads pages of data reactively via a suspend loader.
val comp = paginatedList(
pageSize = 50,
loadThreshold = 10,
loadPage = { offset, limit -> fetchItems(offset, limit) },
renderItem = { item, focused -> if (focused) text(item.name).inverted() else text(item.name) },
renderHeader = { name -> hbox(text(" $name").bold(), filler()) }
)A loading row appears at the bottom while the next page fetches. Fuzzy search / filters over all currently loaded items.
Renders a pipeline of steps with status icons, spinners, and expandable output.
val comp = stepProgress(
getState = {
StepProgressState(
steps = listOf(
ProgressStep("Build", StepStatus.Done, output = buildLogs),
ProgressStep("Test", StepStatus.Running)
),
spinnerTick = currentTick
)
}
)Keys: j/k navigate, →/l expand output, ←/h collapse, Space toggle.
Each built-in view can be styled individually by passing a specific *Style config class. By default, styles fall back to the active theme's colors defined in Theme.current.
val comp = list(
getEntries = { entries },
renderItem = { item, focused -> text(item) },
renderHeader = { text(it) },
style = ListStyle(
focusedItemForeground = Color.Black,
focusedItemBackground = Color.Yellow,
headerForeground = Color.Cyan
)
)ListStyle: focusedItemForeground, focusedItemBackground, headerForeground, scrollThumb, searchHighlight
TableStyle: headerForeground, focusedRowForeground, focusedRowBackground, sortIndicatorColor, scrollThumb, borderStyle
TreeStyle: focusedNodeForeground, focusedNodeBackground, expandedIcon, collapsedIcon, leafIndent, scrollThumb
SplitStyle: activeTitleForeground, inactiveTitleForeground, borderStyle, activeBorderStyle
DashboardStyle: focusedTitleForeground, unfocusedTitleForeground, borderStyle, focusedBorderStyle
PagerStyle: searchHighlight, lineNumberColor, scrollThumb
StepProgressStyle: pendingColor, runningColor, doneColor, failedColor, skippedColor
FilePickerStyle: directoryColor, fileColor, pathColor, scrollThumb
TextEditorStyle: lineNumbersColor, cursorForeground, cursorBackground, scrollThumb
To configure global colors, subclass or instantiate ThemeColors and assign it:
Theme.current = ThemeColors(
accent = Color.Green,
border = Color.GrayDark
)Every built-in view accepts an optional *Keybindings parameter that lets you remap any key without subclassing:
val comp = list(
getEntries = { entries },
renderItem = { item, focused -> text(item) },
renderHeader = { text(it) },
keybindings = ListKeybindings(
moveUpChars = listOf("k", "p"),
moveDownChars = listOf("j", "n"),
)
)Available keybinding types: ListKeybindings, TableKeybindings, TreeKeybindings, PagerKeybindings, StepProgressKeybindings, FilePickerKeybindings, DashboardKeybindings, SplitKeybindings, TextEditorKeybindings.
| Shortcut | Overlay |
|---|---|
? |
Help — all registered shortcuts + framework defaults |
Ctrl+P |
Command palette — fuzzy search over registered shortcuts |
Ctrl+N |
Notification history — scroll with j/k, Esc to close |
Ctrl+L |
Log viewer — scroll with j/k/g/G, Esc to close |
Ctrl+Alt+P |
Performance overlay — FPS, frame time, stack depth, terminal size |
navigator.showDialog(Dialog.Alert(title = "Info", message = "Done."))
navigator.showDialog(Dialog.Confirm(
title = "Delete?",
message = "This cannot be undone.",
onConfirm = { doDelete() },
onCancel = { /* nothing */ },
))
navigator.showDialog(Dialog.Prompt(
title = "Enter name",
placeholder = "Name",
onSubmit = { name -> save(name) },
))navigator.notify("Saved", Toast.SHORT, Toast.Type.Success)
navigator.notify("Low disk space", Toast.SHORT, Toast.Type.Warning)
navigator.notify("Load failed", Toast.LONG, Toast.Type.Error)
navigator.notify("FYI", Toast.SHORT, Toast.Type.Info) // defaultUp to 3 toasts are stacked simultaneously. Each shows an animated countdown border. All toasts are accessible via Ctrl+N notification history.
Logger.debug("Verbose detail")
Logger.info("User opened settings")
Logger.warn("Config missing, using defaults")
Logger.error("Connection refused")Logs are kept in a 1000-entry in-memory ring buffer, rendered inside a log panel overlay (Ctrl+L).
Persistent key-value store backed by ~/.config/<appName>/prefs.properties. The Preferences instance is created automatically by runApp and is accessible via AppContext.preferences.
val count = preferences.getInt("launch.count", default = 0)
preferences.setInt("launch.count", count + 1)
preferences.setString("last.file", "/path/to/file")
val path = preferences.getString("last.file", default = "")
preferences.setBoolean("dark.mode", true)Preferences are saved automatically on app exit.
Generic undo/redo stack (used internally by textEditor).
val history = UndoRedoStack(initial = emptyList<String>(), maxSize = 100)
history.push(newState)
history.undo()
history.redo()
history.reset(newInitial)Switches between two layouts based on terminal width or height. Both variants are called as extensions on AppContext.
// Switch on terminal width
val comp = responsiveHorizontal(
breakpoint = 120,
narrow = { buildNarrowLayout() },
wide = { buildWideLayout() },
)
// Switch on terminal height
val comp = responsiveVertical(
breakpoint = 30,
short = { buildShortLayout() },
tall = { buildTallLayout() },
)Viewport measures the actual pixel-dimensions of the slot assigned to a component after layout. It is created via AppContext.viewport() and used internally by scrollable views; it can also be used in custom renderers when you need the true available height or width.
val vp = viewport()
// vp.width / vp.height — exact slot size after the first render frame# Build (macOS ARM64)
./gradlew linkDebugExecutableMacosArm64
# Run the demo app
./build/bin/macosArm64/debugExecutable/ftxui-kt-framework.kexe
# Build (Linux x64)
./gradlew linkDebugExecutableLinuxX64Ctrl+N, Ctrl+L, Ctrl+P, and ? are reserved by the framework; do not register them on your components.enableCtrlZ = true is required in runApp() to use undo/redo in textEditor.navigator.scopeFor(comp) rather than GlobalScope for coroutines that collect ViewModel state — the scope is cancelled automatically when the component is popped.navigator.onTopChanged(comp) to pause background work (polling, refresh loops) while a screen is obscured by another.