@f0ursqu4r3/term0

Vue 3 components for a monochrome terminal look on a character-cell grid
468
Install
npm install @f0ursqu4r3/term0
Latest Version:0.4.0
Vue:3
License:MIT
Last Updated:Sep 30, 2026
Links: npm
Maintainer: f0ursqu4r3

term0

Showcase: https://f0ursqu4r3.github.io/term0/ (Vue) and https://f0ursqu4r3.github.io/term0/elements.html (HTML and htmx)

Vue 3 components for a monochrome terminal look on a character-cell grid. Every length is a cell (ch) or a line (lh). Every colour comes from a Ghostty palette.

Coding agents: read AGENT_GUIDE.md. It ships in the package at node_modules/@f0ursqu4r3/term0/AGENT_GUIDE.md.

Install

bun add @f0ursqu4r3/term0

Use

import '@f0ursqu4r3/term0/style.css'
<script setup lang="ts">
import { TermRoot, TPane, TLine, TMeter, usePalette } from '@f0ursqu4r3/term0'

usePalette() // apply the stored palette at start
</script>

<template>
  <TermRoot>
    <TPane title="Status">
      <TLine
        :runs="[
          { text: 'LINK ', tone: 'dim' },
          { text: 'UP', tone: 'ok' },
        ]" />
      <TMeter :value="5" :total="8" :width="24" />
    </TPane>
  </TermRoot>
</template>

TermRoot sets the font, the cell grid, the palette and a reset. These apply only inside the root, so a root can sit in a normal page. For a full-page app, also import @f0ursqu4r3/term0/base.css. It applies the same styles to html.

For an app that takes the whole window, set absolute. The root fills the window in whole cells and lines, so the grid stays exact at every window size. The remainder, less than one cell or line, sits after the root by default. align moves it: 'start', 'center' or 'end' for both axes, or [x, y] for each axis.

<TermRoot absolute align="center">...</TermRoot>
<TermRoot absolute :align="['center', 'start']">...</TermRoot>

All package CSS is in @layer term0. Your own unlayered CSS always wins.

Components

Component Purpose
TermRoot Root. Prop palette (a Palette or Ghostty text) applies to this root only. absolute, align.
TPane Box with the title in the top rule.
TLine, TRun One console line of toned runs.
TKey LABEL value link or button.
TCursor Blinking block cursor.
TButton inverse or plain (hover-inverse).
TInput, TSelect, TCheckbox Form controls with v-model.
TDialog Modal pane on the native <dialog>, v-model:open.
TList Selectable rows, v-model:selected. Arrow keys, Home, End and letters move. height scrolls.
TTable Fixed-width columns in cells. Sort, select and virtual rows. See Tables.
TMeter, TSparkline, TTimeline Text charts.
TChart, TBars Multi-row live charts. See Live charts.
TPaletteEditor Ghostty palette editor.
TTabs Tab bar and panel, v-model. Arrow keys, Home and End move between tabs.
TRadioGroup (•) / ( ) over native radios, v-model.
TTextarea Multi-line input. rows in lines, width in cells.
TTree ├─ / └─ tree. v-model:expanded, v-model:selected. Right and Left arrows expand and collapse.
TLog Scrolling log with time and level. It follows new lines while scrolled to the bottom. ansi parses colour codes. Virtual rows.
TPrompt Shell prompt with history and Tab completion. See Shell prompt.
TSpinner One-cell activity glyph: braille, line, block or your own frames. Still under reduced motion.
TBadge Inline inverse label.
TKbd Key cap.
TRule ── LABEL ──── divider.
TAlert Box in a palette role: info, ok, warn, alarm.
TStatusBar Inverse bar with left and #right slots.
TFields Aligned label and value pairs, with an optional · leader.
TPanelGroup, TPanel Panels in a row or column, sized in cells. border="shared" joins panes; resizable drags.
TCommandPalette, TMenu Fuzzy command list and menu button. See Keys and commands.
TKeyHints Footer of key caps and labels: q quit / find.
TToasts Stack of TAlert toasts in a corner. See Toasts.
THeatmap Grid of ░▒▓█ shades, with row labels and a legend.
TProgress Bar, percent, rate and time left: [████ ] 50% 9.6MB/s ETA 0:26.
TDiff + ok, - alarm, @@ info. A unified patch, or before and after.
TScrollbar One-cell scrollbar beside a scrolling element. See Scrollbars.
TCanvas Draws a CellBuffer (text, boxes, lines, braille dots). See Drawing.
TGauge A half-circle gauge drawn with braille dots.
TSplash Big letters for a heading, in the block (5 lines) or small (3 lines) font. Named by its text for screen readers.
TStepper Step indicator, [✓] Build ── [2] Test ── [3] Ship, in a row or a column. It shows progress and takes no input.
TForm, TField A <form novalidate> with labelled rows. TField puts a label, a control and an error or hint on the cell grid. TForm shows an error summary and emits submit or invalid.
TCalendar Month calendar on the cell grid. v-model is a YYYY-MM-DD string. min and max limit the days. Full keyboard control.
TMarkdown Markdown as text on the cell grid. Headings, lists, code blocks, quotes, rules, code, strong, em and links. HTML shows as text.

Tones: dim, faint, alarm, ok, warn, info, inverse, inverse-alarm, inverse-warn.

The glyph functions are also exported: meter, sparkline, occupancyGlyph, fit, right, axis, the line() builder, the timeline helpers Axis, counts, coverage and density, and the chart helpers plot, bars, chartAxis, chartSummary, barsSummary, resolveScale and ticks, and heatmap, ansiRuns, AnsiParser, diffLines, parsePatch, duration, percent and eta.

Live charts

TChart draws one or more series as a line or an area, height lines tall. TBars draws one bar per value. Both redraw when their values change and hold no history. For live data, keep the samples in useSeries: a ring buffer that holds the newest capacity values.

<script setup lang="ts">
import { TChart, TBars, useSeries } from '@f0ursqu4r3/term0'

const cpu = useSeries(120)
setInterval(() => cpu.push(readCpu()), 1000)
</script>

<template>
  <TChart
    :series="[{ values: cpu.values.value, tone: 'ok', label: 'cpu %' }]"
    :height="8"
    :width="60"
    :min="0"
    :max="100"
    y-axis
    legend
    :thresholds="[{ value: 80, tone: 'alarm', rule: true }]" />
  <TBars :values="cpu.values.value.slice(-12)" :height="4" :bar-width="3" :gap="1" />
</template>

TChart options:

Prop Default Purpose
values or series One series, or { values, tone, label } for each series. A later series draws on top.
glyphs 'quadrant' quadrant (2×2 per cell), block (1 sample per cell, 1/8 line) or braille (2×4).
fill 'line' line or area.
height, width 4, samples Lines and cells. width defaults to the longest series.
align 'right' Where the newest sample sits.
min, max 'auto' Fix both for a live chart, so the scale does not jump.
yAxis, ticks off, ≤ 5 Label column with ┤ at each tick. formatTick formats the labels.
xAxis [cell, label] ticks under the plot.
thresholds [] { value, tone, rule }: cells at or above value take tone. rule draws ┄ at value.
legend off A row with each series label in its tone.
label summary aria-label. The default gives each series' last value, min and max.

TBars takes values, labels, height, barWidth, gap, min, max, baseline, tone (a tone or a function of the value and index), thresholds, showValues, yAxis, ticks, formatTick and label. Values below baseline grow down.

Every row of a chart has the same width in cells. A value that is not a finite number is a gap.

The bundled font has no braille glyphs, so glyphs="braille" uses a fallback font. Each braille glyph sits in a one-cell box, so the grid holds, but the glyphs look different from the rest of the page. Use quadrant to stay in the bundled font.

Panel groups

<TPanelGroup border="shared" resizable>
  <TPanel v-model:size="side" :min="12"><TPane title="Files">...</TPane></TPanel>
  <TPanel fill><TPane title="Editor">...</TPane></TPanel>
</TPanelGroup>

TPanelGroup puts any number of panels next to each other. It fills its parent, so give the parent a size. size, min and max are cells in a row and lines in a column. The first fill panel takes the remaining space. Without fill, the last panel takes it.

A pane border on a seam is the divider. With border="separate", two panes keep a one-cell gap between their borders. With border="shared", they use one border.

The dividers are fixed by default. With resizable, a drag or an arrow key moves a divider by whole cells, and Home and End move it to its limits.

A TPane in a panel draws its top and bottom rules through the middle of the first and last line. Its body is the whole lines between them, so give a pane panel :min="3" or more. The lower pane of a shared column has no top line of its own, so it needs :min="2".

Keys and commands

useHotkeys calls a handler when its combo is pressed, while the component is mounted. A combo is a key name with modifiers: q, /, ?, escape, ctrl+k, shift+tab. mod is Meta on Apple platforms and Ctrl elsewhere. Keys typed into a text field are skipped, unless the combo is in inInputs.

<script setup lang="ts">
import { TCommandPalette, TKeyHints, TMenu, useHotkeys, type Command } from '@f0ursqu4r3/term0'

const commands: Command[] = [
  { id: 'open', label: 'Open file', group: 'File', hint: 'mod+o' },
  { id: 'quit', label: 'Quit', hint: 'q' },
]
useHotkeys({ q: quit, 'mod+o': openFile }, { inInputs: ['mod+o'] })
</script>

<template>
  <TMenu label="File" :items="commands" @select="run" />
  <TCommandPalette :commands="commands" @run="(command) => run(command.id)" />
  <TKeyHints
    :hints="[
      { key: 'mod+k', label: 'commands' },
      { key: 'q', label: 'quit' },
    ]" />
</template>

TCommandPalette opens with its hotkey (default mod+k) or v-model:open. It matches the query against group: label with characters in order; runs and word starts rank first. Up and Down move, Enter runs, Escape closes. fuzzyMatch, fuzzyFilter and highlight are exported for your own filters.

TMenu opens under its button. Up, Down, Home and End move and skip disabled items. Enter or a click selects; Escape, Tab or a click outside closes it. hint shows a combo on the right. comboLabel gives the text for a combo: ⌘K on Apple, Ctrl+K elsewhere.

Shell prompt

<TLog :entries="output" :height="8" />
<TPrompt v-model:history="history" :complete="complete" aria-label="Shell" @submit="run" />

Enter emits submit and adds the line to history (the newest limit lines, default 100). Up and Down walk the history and come back to the draft. Tab calls complete with the text before the caret. It returns whole-line candidates: one replaces the line, several fill their common prefix and show under the prompt. Attributes go to the <input>.

ANSI output

TLog shows CLI output with its colours when you set ansi. The style carries from one entry to the next, as in a terminal. A bare \r rewrites the line, so progress bars show their last state. Every other escape sequence is removed. The text never becomes HTML.

<TLog :entries="output" ansi />
<TLog :entries="output" ansi ansi-colours="palette" />

ansi-colours picks how colours render:

  • tone (default): red is alarm, green ok, yellow warn, blue info, bright black faint. Other colours are plain. The look stays monochrome.
  • palette: the 16 ANSI colours of the palette, as t0-fg-N and t0-bg-N. Bold makes colours 0 to 7 bright. 256-colour and truecolor codes use the nearest of the 16.

For a raw stream, such as fetch or a WebSocket, give the chunks to an AnsiParser. It returns the lines each chunk completes and keeps a partial line or escape for the next chunk.

import { ref } from 'vue'
import { AnsiParser, type LogEntry } from '@f0ursqu4r3/term0'

const parser = new AnsiParser({ colours: 'tone' })
const output = ref<LogEntry[]>([])

socket.onmessage = ({ data }) => {
  const lines = parser.push(data).map((runs) => ({ text: '', runs }))
  output.value = [...output.value, ...lines].slice(-1000)
}

ansiRuns(text) parses one string into Run[] for TLine. LogEntry.runs shows runs in place of text.

With ansi, TLog caches the parse for each entry object. Do not change an entry in place: replace it with a new object. Keep the entry objects you already have when you append, so only new entries parse. Each entry is one row: an escape sequence never continues into the next entry.

Long logs

TLog wraps each entry on the cell grid and draws only the rows in view, plus overscan rows (default 5) above and below. A log of 100 000 entries scrolls and appends without delay. Two limits follow from this:

  • Browser find (Ctrl+F) and text selection reach only the drawn rows.
  • The wrap counts one cell for each code point, so wide characters (CJK, most emoji) take two cells on screen but count as one.

Toasts

import { toast } from '@f0ursqu4r3/term0'

toast('Saved.', { tone: 'ok' })
toast('Deploy failed.', { tone: 'alarm', title: 'Deploy', timeout: 0 })

Put one <TToasts /> in the app. It shows the shared stack in a corner (default bottom-end), newest nearest the edge. A toast leaves after timeout ms (default 4000); 0 stays until its × button. The stack keeps the newest 5. createToasts(limit) makes a separate stack for <TToasts :stack="stack" />.

Tables

<TTable
  v-model:sort="sort"
  v-model:selected="pid"
  :columns="[{ key: 'pid', label: 'PID', width: 6, sortable: true }]"
  :rows="rows"
  row-key="pid"
  :height="12" />

A sortable column sorts on a header click: ascending, descending, then unsorted. The header shows ▲ or ▼ and sets aria-sort. Numbers sort as numbers and text sorts with numeric parts; compare on the column overrides this.

rowKey makes rows selectable: a click, or Up, Down, Home, End, Page Up and Page Down with the table focused. The selected row is inverse.

height (lines, header included) scrolls the body under a sticky header and draws only the rows in view, plus overscan rows each side (default 5). Each row is then one line, so cell slots must not wrap.

Scrollbars

The package CSS draws native scrollbars one cell wide in palette greys: a void track set off by a rule, and a faint thumb. Some browsers ignore this styling, for example Safari overlay scrollbars. For the same scrollbar in every browser, set scrollbar="cell" on TLog, or on TList and TTable with a height. The thumb is at least one line tall. Drag it, click the track to page, or use the wheel over it. In a table the track starts under the sticky header.

For your own scrolling element, put <TScrollbar :target="element" :rows="10" /> next to it. offset skips lines at the top. useScroll gives the scroll position as refs.

Drawing

CellBuffer is a grid of character cells. You draw on it, then pass buffer.rows() to TCanvas.

<script setup lang="ts">
import { computed, ref } from 'vue'
import { CellBuffer, TCanvas } from '@f0ursqu4r3/term0'

const tick = ref(0)
const rows = computed(() => {
  tick.value // Read the counter: draw again when it changes.
  return new CellBuffer(24, 6)
    .box(0, 0, 24, 6, 'round', 'dim')
    .put(2, 0, ' Plot ')
    .line(4, 4, 40, 30, 'info')
    .rows()
})
</script>

<template>
  <TCanvas :rows="rows" label="Plot" />
</template>

TCanvas does not watch the buffer. After you draw, call buffer.rows() again and pass the new array.

Big headings

TSplash draws text in big letters. Set font="small" for three lines. Set tone to colour it.

<TSplash text="term0" />
<TSplash text="Ready" font="small" tone="ok" :level="2" />

splash(text, { font }) returns the lines as string[], for your own layout.

Steps

TStepper shows where a process stands. current is the 0-based index of the current step. Done steps are green with ✓. The current step is inverse. Upcoming steps are dim.

<TStepper :steps="['Build', 'Test', 'Ship']" :current="1" />
<TStepper :steps="['Build', 'Test', 'Ship']" :current="1" direction="column" />

Forms

TField lays out one row: the label in a column of cells, the control beside it, and a message under the control. An error shows in the alarm tone with a ! mark. A hint shows in the dim tone. The label points at the first input, select or textarea in the slot. That control also gets aria-describedby for the message, and aria-invalid for an error.

TForm collects the errors. Pass errors (field name to message). Each TField with a matching name shows its message. An alarm summary above the fields shows the count and all messages.

<TForm :errors="errors" @submit="save($event.values)" @invalid="errors = $event.errors">
  <TField name="email" label="Email" hint="We never share it.">
    <TInput type="text" required />
  </TField>
  <TField name="team" label="Team">
    <TSelect v-model="team" :options="teams" />
  </TField>
  <TButton type="submit">Save</TButton>
</TForm>

submit carries { values, form }. The values come from FormData, and the last value of a name wins. invalid carries { errors, form } when required, pattern or another native rule fails. The form never submits itself.

Calendar

TCalendar shows one month. Its v-model is a YYYY-MM-DD string or null. All dates are local calendar days, not UTC instants. Today is info. The selected day is inverse. Days outside min and max are faint and cannot be selected.

<TCalendar v-model="due" min="2026-09-01" :week-start="0" label="Due date" />

Keys on the grid: arrows move by a day or a week, Page Up and Page Down by a month, Home and End to the start and end of the week, Enter and Space select. Month names and weekday initials come from Intl in the browser locale. Set locale to choose another.

Markdown

TMarkdown shows a small subset of CommonMark. It needs no dependency. Give it a string in source.

<TMarkdown :source="notes" label="Release notes" />

The component never uses v-html. HTML in the source shows as text. A link gets an href only when the URL is http:, https:, mailto: or relative. Other links, such as javascript: and data:, show their text and nothing else.

Palette

usePalette() returns shared state: text, palette, custom, error, set(text) and reset(). Valid Ghostty text applies to every root and saves to localStorage (key term0.palette) as it is typed. If the text is not valid, error shows why and the last valid palette stays.

Role Source
Ink, void foreground, background
Dim, faint, rule foreground mixed into background at 64, 42 and 30 %
Alarm, ok, warn, info palette = 1, 2, 3, 4
Cursor cursor-color, cursor-text
Selection selection-background, selection-foreground

Tailwind 4

@import 'tailwindcss';
@import '@f0ursqu4r3/term0/tailwind.css';

This adds the colours (text-dim, bg-void, ...), font-pixel, text-cell, text-cell-2x and the utilities pane, pane-title, inverse, alarm, warn, cells, tty-line, cursor-blink and w-fill. It keeps your theme, but it sets --spacing: 1ch, so p-2 is two cells. Use these utilities inside a TermRoot, and keep the @f0ursqu4r3/term0/style.css import.

HTML and htmx

Vue is the main target. term0 also works without Vue, for server-rendered pages and htmx. It has two parts:

  1. CSS classes. The server renders plain HTML with the classes the Vue components use.
  2. Custom elements for the parts that need JavaScript, from @f0ursqu4r3/term0/elements.

With a bundler:

import '@f0ursqu4r3/term0/style.css'
import '@f0ursqu4r3/term0/elements'

Without a bundler, serve the whole dist/ directory of the package. elements.js imports files from dist/chunks/.

<link rel="stylesheet" href="/term0/style.css" />
<script type="module" src="/term0/elements.js"></script>

The elements use light DOM, so your CSS and htmx swaps reach their contents. The elements keep their state in attributes, so the server can render any state.

CSS-only components

Put class="t0-root" on a container, or on <body> with @f0ursqu4r3/term0/base.css.

The spaces inside these tags render: t0-line and t0-cursor keep white space.

<div class="t0-root">x</div>
<section class="t0-pane"><h2 class="t0-pane-title">Status</h2>x</section>
<div class="t0-line"><span class="t0-tone-dim">LINK </span><span class="t0-tone-ok">UP</span></div>
<button type="button" class="t0-button t0-button-inverse">RUN</button>
<button type="button" class="t0-button t0-hover-inverse">RUN</button>
<span class="t0-badge t0-tone-ok">NEW</span>
<kbd class="t0-kbd">Esc</kbd>
<div class="t0-rule" role="separator" aria-label="ALERTS"><span class="t0-rule-label">ALERTS</span></div>
<div class="t0-alert t0-alert-warn" role="status"><span class="t0-alert-title">warn</span>DISK 91%</div>
<span class="t0-cursor"> </span>

Tones: t0-tone-dim, -faint, -ok, -warn, -alarm, -info, -inverse, -inverse-alarm, -inverse-warn.

Elements

Element Purpose
<t0-meter value total width bracketed> TMeter. bracketed="false" drops the brackets.
<t0-sparkline values> TSparkline. values is numbers separated by spaces or commas.
<t0-spinner frames interval label> TSpinner. frames is braille, line, block or a string of frame characters. Put a visible label after the element.
<t0-tabs value> Behaviour for server-rendered tabs. Fires a bubbling t0-change with detail.value.
<t0-chart values sample ...> TChart with a live buffer. Props in kebab case. values separates series with ;. tones and labels give each series a tone and a label. thresholds is 80:warn:rule, 95:alarm. x-axis is 0:-60s, 59:now.
<t0-bars values sample ...> TBars with a live buffer. Props in kebab case. tone is one tone for every bar.
<t0-log height ansi ...> TLog: wrap on the cell grid, virtual rows, follow mode. See Log with htmx.
<t0-progress value total width rate unit label> TProgress: a bar with its percent. rate (units per second) adds the rate and the ETA.
<t0-heatmap values min max labels shades legend> THeatmap: a grid of shade glyphs. values="1 2 3; 4 5 6".
<t0-toasts corner width limit> TToasts: a stack of TAlert toasts in a corner. Add toasts from script, child rows or the t0-toast event.
<t0-dialog open label> Behaviour for a server-rendered TDialog: opens and closes the native <dialog>, fires t0-close.
<t0-tree label expanded selected> TTree from a nested list: guides, markers, keys, t0-toggle and t0-select events.
<t0-table selected> Sort and row selection for a server-rendered TTable: t0-sort and t0-select events.

A swap that clears the children of t0-meter, t0-sparkline, t0-spinner, t0-chart, t0-bars, t0-progress or t0-heatmap makes them draw again.

t0-chart and t0-bars keep their samples on the element, so a morph keeps the history. el.push(...values) appends to the first series and el.pushTo(index, ...values) to another. The sample attribute appends one value to each series: sample="42;61". A morph sets an attribute only when its value changes, so put a sequence number before an @ to repeat a value: sample="17@42;61". capacity sets the buffer size. The default is width times the samples per cell, or 120.

Log with htmx

<t0-log> reads each child element as a new entry and removes it, so htmx can append rows. data-time and data-level (debug, info, warn, error) set the columns. A t0-tone-* class inside a row sets the tone of its text. Other markup becomes plain text. Row text collapses white space as HTML does. A <pre> row keeps its text as it is.

<t0-log height="10" hx-get="/log/next" hx-trigger="every 2s" hx-swap="beforeend"></t0-log>

The server returns rows such as <div data-time="12:00:01" data-level="warn">disk <span class="t0-tone-alarm">91%</span></div>.

Attributes: height (lines, default 10), ansi, ansi-colours (tone or palette), overscan (default 5) and limit (keep the newest entries; 0, the default, keeps all). With ansi, the text of each row is parsed for ANSI codes. From script, el.push(...entries) adds LogEntry objects, el.write(chunk) adds a raw stream, and el.clear() empties the log.

The element owns its children. Append with beforeend. An innerHTML swap (the htmx default) replaces every entry with the new rows, so a poll can return the whole tail. A morph or an outerHTML swap replaces the element and its entries.

Tabs with htmx

The examples use htmx 4. The server renders the tabs. Give each tab a value, its own hx-get and its own hx-target: htmx 4 does not inherit attributes by default. Arrow keys click the tab, so the request fires for the keyboard too.

<t0-tabs value="disk">
  <div class="t0-tabs-list" role="tablist" aria-label="Views">
    <button role="tab" value="disk" hx-get="/panel/disk" hx-target="#panel">Disk</button>
    <button role="tab" value="log" hx-get="/panel/log" hx-target="#panel">Log</button>
  </div>
  <div id="panel" class="t0-tabs-panel" role="tabpanel" tabindex="0">…</div>
</t0-tabs>

t0-change bubbles, so a t0-tabs inside another tab panel also reaches the outer one. Filter with hx-trigger="t0-change[target === this]" if you listen on the outer element.

Palette without Vue

applyStoredPalette() applies the palette usePalette stores, so Vue and htmx pages on one origin share it. For one root, render the --term-* properties from termProperties(palette) in its style attribute.

<script type="module">
  import { applyStoredPalette } from '/term0/elements.js'
  applyStoredPalette()
</script>

Develop

bun install
bun run dev        # showcase at #/, scoped demo at #/embedded, grid-snapped root at #/absolute
bun run build:playground # static showcase in playground/dist
bun run test
bun run typecheck
bun run lint
bun run fmt:check
bun run build      # writes dist/

The showcase takes ?preset=phosphor (or monotheme, amber, paper) to open with a palette, ?palette to open the editor, and ?absolute to open snapped to the window grid.

Release

dist/ is not in git. The release workflow builds it and publishes to npm.

  1. Set version in package.json and commit.
  2. Tag vX.Y.Z and push the tag. The tag must match version.

The workflow runs lint, format check, typecheck and tests, then npm publish. It uses npm trusted publishing, so the repo stores no npm token.

Licence

MIT. The Departure Mono font is by Helena Zhang, under the SIL Open Font License 1.1 (src/fonts/LICENSE, also shipped in dist/fonts/).

Related Packages

vue-termui

Build terminal apps with Vue 3

24,392
lattice-tui

A keyboard-first, Obsidian-inspired Markdown knowledge base for the terminal.

1,197 0
@vizejs/fresco-native

Vue TUI framework - Native bindings

76,634 908
@vizejs/fresco

Vue TUI framework - Build terminal UIs with Vue.js

71,222 908
@vui-rs/ui

Application-level component library for vui-rs terminal UIs — dialogs, fuzzy sel...

7,598 1

Version History

Version Released Vue License
0.4.0 ^3.5 MIT
0.3.0 ^3.5 MIT
0.2.0 ^3.5 MIT