Skip to content
Back to skills

Terminal Ui

ASecurity

Build MaskShift terminal interfaces in the MaskShift design system — tokens over hex values, one marker gutter per pane, one filled chip per screen, and the ANSI width discipline that keeps a TUI aligned.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 4, 2026
ai-agentsgoshellazure

Works with

  • cursor
  • terminal
  • cli
  • mcp

Security analysis

A100/100

Scanned October 4, 2026

npx -y skills add nafeeur/MaskShift --skill terminal-ui --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Terminal Ui?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Terminal Ui
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/nafeeur-terminal-ui/badge)](https://www.skillsdirectory.com/skills/nafeeur-terminal-ui)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: terminal-ui
description: Build MaskShift terminal interfaces in the MaskShift design system — tokens over hex values, one marker gutter per pane, one filled chip per screen, and the ANSI width discipline that keeps a TUI aligned.
---

# MaskShift terminal design system

A dense terminal interface stays usable only with discipline. Two constraints
hold the system together, and everything below serves them:

1. **Dense is not illegible.** Colour carries meaning, never decoration, and the
   whole palette has a defined fallback at 256 colours, 16 colours and none.
2. **Every row is exactly the terminal width.** A single miscounted column
   shears the entire frame. Alignment is correctness, not polish.

## Tokens, not colours

- Import meaning from `tokens.mjs`; never a hex value, and never `theme.palette.*`
  in a view. A view says `theme.roles.danger`, not "the red one". When one token
  moves, the whole interface moves with it.
- **Crimson is identity and focus only** — the wordmark, the active view tab,
  the pane holding the keyboard. It is never a data value and never a severity.
  Failure has its own red (`roles.danger`) precisely so a red pane cannot read
  as a focused one.
- **Gold is the user** — their turn, their keys, their pending input.
- Capability classes keep fixed accents so the same colour always means the same
  category, everywhere: cyanide for tools, violet for skills, azure for MCP. If
  you add a category, give it a token; if you add a token, wire it to a category.
- Everything else is the neutral ramp. Hierarchy is carried by weight and colour,
  not by inventing a hue. If you reach for a new colour, you probably want
  `heading` / `text` / `label` / `dim` / `muted` instead.
- Define colours once as hex and let the theme degrade them. `Theme.fg`/`bg`
  emit truecolor, 256-colour or 16-colour sequences from the same value.
  **Never hardcode an SGR number**: the 16-colour path has to clamp each channel
  to a single bit, and hand-written codes bypass that.
- `NO_COLOR`, `FORCE_COLOR`, `MASKSHIFT_COLOR=off|basic|full` and a non-TTY
  stdout all have to produce clean, still-aligned monochrome. Colour is never
  load-bearing on its own — pair it with a glyph or a label. Two states that
  differ only in hue are two states nobody can tell apart.

## The grid

- **Every content row is `SPACE.gutter` columns of marker, then text.** Build
  rows with `gutter()` / `row()` from `type.mjs`, even when there is no marker
  to put there. This is the whole reason a speaker rail, a status tick, a
  bullet, a tree glyph and a plain paragraph share one left edge; hand-rolling
  an indent is how a pane ends up with four of them.
- Lay tabular rows out with `columns()`, not with bare `fit` calls and magic
  numbers. Two lists that each invent their own column widths will never line
  up with each other.
- Selection inside a list is a spine (`▌`) in the gutter plus a raised
  background, so it survives a monochrome terminal.

## One label, one chip

- **A name appears once per screen.** The tab strip names the view; a panel's
  top rail then carries what the tab cannot — the session title, the shell's
  directory, a section switcher. Printing the view's name on the panel under
  its own tab is the single most repetitive thing a layout can do.
- **Exactly one filled chip per screen**: the active view tab. Everything else
  is text on the surface it belongs to. A modal's primary action is the only
  exception, and only because there is no tab strip on screen to confuse it
  with. Section switchers inside a pane are marked by weight and an underline.
- Focus is the frame, and the frame alone: a light hairline when idle, a heavy
  frame in a *dark* red when focused. Mixing a border with full crimson traces a
  bright rectangle around whatever the user is already looking at.
- **Everything is written in ordinary sentence case.** Never upper-case labels,
  a model's headings or a user's prose; weight and colour carry the hierarchy.

## Status and motion

- Every subsystem state — runs, plan steps, servers, processes, tool results —
  resolves through `statusOf()` in `status.mjs`. Seven kinds, each fixing a tone
  and a glyph. Do not write another `STATUS_TONES` map in a view; add to the
  vocabulary instead.
- Animation is a function of the wall clock in `motion.mjs`, never of a frame
  counter, so the interface looks the same at 8fps over SSH and freezes whole
  for a headless capture. Two spinners driven by their own counters drift apart
  on screen.
- Nothing animates for decoration. Each animation answers a question: is this
  alive (a breathing lamp), is it working or stuck (a sweep along the focused
  rail), did I miss that (a toast fading), which step is in flight (an inline
  spinner).
- `MASKSHIFT_ASCII=1` and a non-UTF-8 locale swap every box-drawing glyph for
  ASCII. Any new glyph needs an entry in both `MARKS.unicode` and `MARKS.ascii`.

## Width discipline

This is where TUIs actually break.

- Measure with `visibleWidth`, never `String.length`. Escape sequences are
  zero-width, CJK and emoji are two columns, and combining marks are zero.
- Pad and clip with `fit`/`truncate`/`padEnd` — all ANSI-aware. Slicing a styled
  string by index cuts an escape sequence in half and bleeds colour into the
  rest of the frame.
- A panel's body rows must be exactly `width - 2 - 2*padding` columns and
  exactly `height - 2` rows. Off-by-one here is the difference between a frame
  and a smear. Assert it in tests rather than eyeballing it.
- Reserve the space a component needs *before* spending it on optional chrome.
  A header that adds telemetry chips left to right will starve the workspace
  name; compute the reserve first, then add chips while they fit.
- Below a threshold, drop whole components rather than shrinking everything: the
  rail hides under 108 columns, and the detail pane under 92. A view that is
  merely cramped at 80×24 is a bug.

## Rendering

- Build a frame as an array of styled lines and diff it against the previous
  frame; rewrite only the rows that changed. Full repaints flicker over SSH and
  fight the terminal's own scrollback.
- Take the alternate screen on start and always give it back — including on a
  crash. Restore the cursor and reset SGR in the same teardown.
- Throttle repaints (one per tick, coalesced through `setImmediate`) instead of
  painting on every event. A run emits hundreds of events a second.
- Keep spinners and elapsed timers on one shared interval so the whole frame
  advances together.

## Input

- Decode raw mode yourself: control keys, CSI sequences, modifier parameters
  (`ESC[1;5A` is ctrl+up), and bracketed paste — a pasted prompt must arrive as
  one event, not a thousand keystrokes.
- A lone `ESC` is ambiguous until the next byte arrives. Buffer it and resolve
  it on a short timer, or every arrow key registers as an escape.
- Overlays own the keyboard completely while open. Global shortcuts must not
  fire underneath a dialog, and every overlay dismisses on `esc`.
- Every action needs a discoverable route: a key, a slash command, and an entry
  in the command palette. Nothing may be reachable only by a key you memorised.

## Layout and correctness

- Dense but grouped. Every panel needs a title, a state, a primary action, and
  an empty state that says what to do next.
- The hint bar is contextual: it shows the keys for the focused component, not a
  fixed list. If a key is not in the hint bar or the palette, it does not exist.
- Long output scrolls inside its own viewport with a visible scrollbar column;
  the frame itself never scrolls.
- A shell-like transcript anchors to the bottom, so new output appears next to
  the prompt. A catalogue anchors to the top.
- Verify by rendering real frames at several sizes and asserting every row's
  width and the frame's height — the terminal equivalent of checking for
  horizontal overflow. Reading the layout code is not verification; every
  alignment bug is invisible in source and obvious in a rendered frame.
- Assert the design rules, not just the geometry. `tests/tui.test.mjs` checks
  that every transcript row clears the gutter before its text starts, and that
  a view's name appears on exactly one row of its own screen — both are bugs
  that survive any width assertion and that no one notices while writing the
  code that causes them.
- Look at the output. `npm run capture` renders real frames to SVG through the
  same renderer; a palette that reads fine as a list of hex values can still put
  the loudest thing on screen around the thing the user is already looking
  at.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…