[READ-ONLY] Mirror of https://github.com/bombshell-dev/tty. Platform independent 2D layout engine for terminal applications based on Clay
5

Configure Feed

Select the types of activity you want to include in your feed.

✨ example(term): interactive text input demonstrating caret

Charles Lowell (Jul 2, 2026, 6:29 PM +0300) 00d0286b b0597fcc

+192
+192
examples/text-input/index.ts
··· 1 + import { Buffer } from "node:buffer"; 2 + import process from "node:process"; 3 + import { each, ensure, main, until } from "effection"; 4 + import { 5 + close, 6 + createTerm, 7 + fixed, 8 + grow, 9 + type KeyEvent, 10 + type Op, 11 + open, 12 + rgba, 13 + text, 14 + } from "../../mod.ts"; 15 + import { 16 + alternateBuffer, 17 + progressiveInput, 18 + settings, 19 + } from "../../settings.ts"; 20 + import { useInput } from "../use-input.ts"; 21 + import { useStdin } from "../use-stdin.ts"; 22 + 23 + const bg = rgba(20, 20, 30); 24 + const inputBg = rgba(35, 35, 50); 25 + const border = rgba(80, 100, 160); 26 + const label = rgba(180, 180, 200); 27 + const hint = rgba(80, 80, 100); 28 + 29 + await main(function* () { 30 + let { columns, rows } = terminalSize(); 31 + 32 + setRawMode(true); 33 + 34 + let stdin = yield* useStdin(); 35 + let input = useInput(stdin); 36 + 37 + let term = yield* until(createTerm({ width: columns, height: rows })); 38 + 39 + let tty = settings(alternateBuffer(), progressiveInput(1)); 40 + writeStdout(tty.apply); 41 + 42 + let value = ""; 43 + let caret = 0; 44 + 45 + yield* ensure(() => { 46 + setRawMode(false); 47 + writeStdout(tty.revert); 48 + }); 49 + 50 + let { output } = term.render(frame(value, caret)); 51 + writeStdout(output); 52 + 53 + for (let event of yield* each(input)) { 54 + if (event.type === "keydown") { 55 + let key = event as KeyEvent; 56 + 57 + if (key.ctrl && key.key === "c") { 58 + break; 59 + } 60 + 61 + if (key.key === "Escape") { 62 + break; 63 + } 64 + 65 + if (key.key === "ArrowLeft") { 66 + if (caret > 0) { 67 + caret--; 68 + } 69 + } else if (key.key === "ArrowRight") { 70 + if (caret < [...value].length) { 71 + caret++; 72 + } 73 + } else if (key.key === "Backspace") { 74 + if (caret > 0) { 75 + let chars = [...value]; 76 + chars.splice(caret - 1, 1); 77 + value = chars.join(""); 78 + caret--; 79 + } 80 + } else if ( 81 + key.key.length === 1 && 82 + !key.ctrl && 83 + !key.alt 84 + ) { 85 + let chars = [...value]; 86 + chars.splice(caret, 0, key.key); 87 + value = chars.join(""); 88 + caret++; 89 + } 90 + 91 + ({ output } = term.render(frame(value, caret))); 92 + writeStdout(output); 93 + } 94 + 95 + yield* each.next(); 96 + } 97 + }); 98 + 99 + function frame(value: string, caret: number): Op[] { 100 + let ops: Op[] = []; 101 + 102 + ops.push( 103 + open("root", { 104 + layout: { 105 + width: grow(), 106 + height: grow(), 107 + direction: "ttb", 108 + alignX: "center", 109 + alignY: "center", 110 + padding: { left: 4, right: 4, top: 2, bottom: 2 }, 111 + }, 112 + bg, 113 + }), 114 + ); 115 + 116 + // Input row: "Name:" label + input box 117 + ops.push( 118 + open("input-row", { 119 + layout: { 120 + direction: "ltr", 121 + gap: 2, 122 + height: fixed(3), 123 + alignY: "center", 124 + }, 125 + }), 126 + ); 127 + 128 + ops.push( 129 + open("label", { 130 + layout: { 131 + width: fixed(6), 132 + height: fixed(1), 133 + alignX: "right", 134 + alignY: "center", 135 + }, 136 + }), 137 + text("Name:", { color: label }), 138 + close(), 139 + ); 140 + 141 + ops.push( 142 + open("input-box", { 143 + layout: { 144 + width: fixed(40), 145 + height: fixed(1), 146 + padding: { left: 1, right: 1 }, 147 + alignY: "center", 148 + }, 149 + bg: inputBg, 150 + border: { color: border, left: 1, right: 1, top: 1, bottom: 1 }, 151 + }), 152 + text(value, { color: label, caret }), 153 + close(), 154 + ); 155 + 156 + ops.push(close()); // input-row 157 + 158 + // Hint line 159 + ops.push( 160 + open("hint", { 161 + layout: { 162 + height: fixed(1), 163 + padding: { top: 1 }, 164 + }, 165 + }), 166 + text("← → move Backspace delete Esc or Ctrl+C exit", { color: hint }), 167 + close(), 168 + ); 169 + 170 + ops.push(close()); // root 171 + 172 + return ops; 173 + } 174 + 175 + function terminalSize(): { columns: number; rows: number } { 176 + return process.stdout.isTTY 177 + ? { 178 + columns: process.stdout.columns ?? 80, 179 + rows: process.stdout.rows ?? 24, 180 + } 181 + : { columns: 80, rows: 24 }; 182 + } 183 + 184 + function setRawMode(enabled: boolean): void { 185 + if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") { 186 + process.stdin.setRawMode(enabled); 187 + } 188 + } 189 + 190 + function writeStdout(bytes: Uint8Array): void { 191 + process.stdout.write(Buffer.from(bytes)); 192 + }