What We’ll Build
We’ll create a task manager application featuring:- A list of tasks with selection
- Input field for adding new tasks
- Status bar showing key bindings
- Responsive layout that adapts to terminal size
Prerequisites
Make sure you have Bun installed:import { createCliRenderer } from "@opentui/core"
const renderer = await createCliRenderer({
exitOnCtrlC: true,
targetFps: 30,
})
renderer.setBackgroundColor("#001122")
renderer.start()
console.log("Task Manager initialized!")
import { createCliRenderer, BoxRenderable, TextRenderable } from "@opentui/core"
const renderer = await createCliRenderer({
exitOnCtrlC: true,
targetFps: 30,
})
renderer.setBackgroundColor("#001122")
// Create header
const header = new BoxRenderable(renderer, {
id: "header",
width: "auto",
height: 3,
backgroundColor: "#3b82f6",
borderStyle: "single",
alignItems: "center",
border: true,
})
const headerText = new TextRenderable(renderer, {
id: "header-text",
content: "TASK MANAGER",
fg: "#ffffff",
})
header.add(headerText)
renderer.root.add(header)
renderer.start()
import {
createCliRenderer,
BoxRenderable,
TextRenderable,
SelectRenderable,
SelectRenderableEvents,
} from "@opentui/core"
// ... previous header code ...
// Task data
const tasks = [
{ name: "Write documentation", completed: false },
{ name: "Fix bug in parser", completed: true },
{ name: "Review pull requests", completed: false },
]
// Create task list
const taskList = new SelectRenderable(renderer, {
id: "task-list",
width: "auto",
height: "auto",
flexGrow: 1,
options: tasks.map((task, i) => ({
name: `${task.completed ? "✓" : "○"} ${task.name}`,
description: task.completed ? "Completed" : "Pending",
})),
position: "relative",
})
taskList.on(SelectRenderableEvents.ITEM_SELECTED, (index, option) => {
console.log(`Selected task: ${tasks[index].name}`)
// Toggle completion
tasks[index].completed = !tasks[index].completed
updateTaskList()
})
function updateTaskList() {
taskList.setOptions(
tasks.map((task) => ({
name: `${task.completed ? "✓" : "○"} ${task.name}`,
description: task.completed ? "Completed" : "Pending",
}))
)
}
renderer.root.add(taskList)
taskList.focus()
import {
createCliRenderer,
BoxRenderable,
TextRenderable,
SelectRenderable,
SelectRenderableEvents,
InputRenderable,
InputRenderableEvents,
} from "@opentui/core"
// ... previous code ...
// Create input container
const inputContainer = new BoxRenderable(renderer, {
id: "input-container",
width: "auto",
height: 5,
borderStyle: "single",
border: true,
padding: 1,
})
const inputLabel = new TextRenderable(renderer, {
id: "input-label",
content: "New Task:",
fg: "#00FFFF",
})
const taskInput = new InputRenderable(renderer, {
id: "task-input",
width: "auto",
placeholder: "Enter task name...",
focusedBackgroundColor: "#1a1a1a",
})
taskInput.on(InputRenderableEvents.ENTER, (value) => {
if (value.trim()) {
tasks.push({ name: value, completed: false })
updateTaskList()
taskInput.value = ""
console.log(`Added task: ${value}`)
}
})
inputContainer.add(inputLabel)
inputContainer.add(taskInput)
renderer.root.add(inputContainer)
// Create footer
const footer = new BoxRenderable(renderer, {
id: "footer",
width: "auto",
height: 3,
backgroundColor: "#1e40af",
borderStyle: "single",
alignItems: "center",
justifyContent: "center",
border: true,
})
const footerText = new TextRenderable(renderer, {
id: "footer-text",
content: "↑/↓: Navigate | Enter: Toggle | Tab: Switch Focus | Ctrl+C: Exit",
fg: "#ffffff",
})
footer.add(footerText)
renderer.root.add(footer)
import type { KeyEvent } from "@opentui/core"
let focusedElement: "list" | "input" = "list"
renderer.keyInput.on("keypress", (key: KeyEvent) => {
if (key.name === "tab") {
if (focusedElement === "list") {
taskList.blur()
taskInput.focus()
focusedElement = "input"
} else {
taskInput.blur()
taskList.focus()
focusedElement = "list"
}
}
})
// Add a status indicator
const statusText = new TextRenderable(renderer, {
id: "status",
content: "",
position: "absolute",
right: 2,
top: 1,
fg: "#00FF00",
})
renderer.root.add(statusText)
// Update status every second
setInterval(() => {
const completed = tasks.filter((t) => t.completed).length
const total = tasks.length
statusText.content = `${completed}/${total} completed`
}, 1000)
import {
createCliRenderer,
BoxRenderable,
TextRenderable,
SelectRenderable,
SelectRenderableEvents,
InputRenderable,
InputRenderableEvents,
type KeyEvent,
} from "@opentui/core"
const renderer = await createCliRenderer({
exitOnCtrlC: true,
targetFps: 30,
})
renderer.setBackgroundColor("#001122")
// Data
const tasks = [
{ name: "Write documentation", completed: false },
{ name: "Fix bug in parser", completed: true },
{ name: "Review pull requests", completed: false },
]
let focusedElement: "list" | "input" = "list"
// Header
const header = new BoxRenderable(renderer, {
id: "header",
width: "auto",
height: 3,
backgroundColor: "#3b82f6",
borderStyle: "single",
alignItems: "center",
border: true,
})
const headerText = new TextRenderable(renderer, {
id: "header-text",
content: "TASK MANAGER",
fg: "#ffffff",
})
header.add(headerText)
// Task List
const taskList = new SelectRenderable(renderer, {
id: "task-list",
width: "auto",
height: "auto",
flexGrow: 1,
options: [],
})
function updateTaskList() {
taskList.setOptions(
tasks.map((task) => ({
name: `${task.completed ? "✓" : "○"} ${task.name}`,
description: task.completed ? "Completed" : "Pending",
}))
)
}
taskList.on(SelectRenderableEvents.ITEM_SELECTED, (index) => {
tasks[index].completed = !tasks[index].completed
updateTaskList()
})
// Input
const inputContainer = new BoxRenderable(renderer, {
id: "input-container",
width: "auto",
height: 5,
borderStyle: "single",
border: true,
padding: 1,
})
const inputLabel = new TextRenderable(renderer, {
id: "input-label",
content: "New Task:",
fg: "#00FFFF",
})
const taskInput = new InputRenderable(renderer, {
id: "task-input",
width: "auto",
placeholder: "Enter task name...",
})
taskInput.on(InputRenderableEvents.ENTER, (value) => {
if (value.trim()) {
tasks.push({ name: value, completed: false })
updateTaskList()
taskInput.value = ""
}
})
inputContainer.add(inputLabel)
inputContainer.add(taskInput)
// Footer
const footer = new BoxRenderable(renderer, {
id: "footer",
width: "auto",
height: 3,
backgroundColor: "#1e40af",
borderStyle: "single",
alignItems: "center",
justifyContent: "center",
border: true,
})
const footerText = new TextRenderable(renderer, {
id: "footer-text",
content: "↑/↓: Navigate | Enter: Toggle | Tab: Switch Focus | Ctrl+C: Exit",
fg: "#ffffff",
})
footer.add(footerText)
// Status
const statusText = new TextRenderable(renderer, {
id: "status",
content: "",
position: "absolute",
right: 2,
top: 1,
fg: "#00FF00",
})
// Assemble UI
renderer.root.add(header)
renderer.root.add(taskList)
renderer.root.add(inputContainer)
renderer.root.add(footer)
renderer.root.add(statusText)
// Keyboard navigation
renderer.keyInput.on("keypress", (key: KeyEvent) => {
if (key.name === "tab") {
if (focusedElement === "list") {
taskList.blur()
taskInput.focus()
focusedElement = "input"
} else {
taskInput.blur()
taskList.focus()
focusedElement = "list"
}
}
})
// Update status
setInterval(() => {
const completed = tasks.filter((t) => t.completed).length
statusText.content = `${completed}/${tasks.length} completed`
}, 1000)
updateTaskList()
taskList.focus()
renderer.start()
Running Your App
Run your task manager:Next Steps
Styling and Colors
Learn about RGBA colors and text styling
Keyboard and Mouse
Handle user input with key events and mouse interactions
Animations
Add smooth animations with the Timeline system
Console Overlay
Debug your app with the built-in console