agentleFS
derekwisongderekwisong/datui/.claude/skills/ui-style/SKILL.md

ui-style

Skill137 starsChanged 38 days agoMITView on GitHub

The canon for how datui looks, reads and responds. Load before any change to render/, widgets/, the footer, the key registry, keybinds, or before designing a new modal, form, sidebar or screen. Carries the hard rules, the component patterns, the keybind compatibility contract, and the acceptance checklist every UI PR must pass.

4 other projects use a version of this.

Adopted by

Most recently taken up by

  • Other versions4 projects use a version of this.

    Word for word · 0

    None found.

    Edited · 0

    None found.

    Wrote their own version · 4

    1. lxsmnsyc/agent-skills★ 4same name
    2. mia-cx/skills★ 4same name
    3. boris410/claude-skills★ 0same name
    4. getvibekits/vibe-coding-rules★ 0same name

    How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.

  • In this repositoryOne other file it gives its agents.

The file

We split this skill into the rules it gives an agent

6 rules. 0 are common, 2 are distinctive, and 4 are very specific to this project.

  • 0

    Common

    Most of the 3 edited versions we read keep these.

  • 2

    Distinctive

    This author's own choices. Worth a look.

  • 4

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    When the line is short, segments yield in a fixed order (Footer::fit): the dataset's name (middle-elided, and the first to go for a …

    line 144
  2. Very specific:

    Every footer key is a click target (Footer::render_line returns where each landed).

    line 157
  3. Distinctive:

    Rules never signal focus: no heavy rule, no brighter title.

    line 218
  4. Very specific:

    Every form, dialog and option sidebar is a crate::form::Form: an ordered list of fields, each a FieldKind (Text, MultilineText, Choice, …

    line 224
  5. Very specific:

    Footer: every hint, ? keys, the filters/sort (s) and query (:) are click targets (Drawn), only where those keys mean that.

    line 290
  6. Distinctive:

    Every message earns its interruption level.

    line 298

“Common” is measured: kept by at least 6 in 10 of the 3 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

The datui visual style

datui is a terminal tool for looking at data. The data is the interface; everything else is chrome, and chrome whispers. When a screen feels heavy, the cause is almost always decoration doing a job that alignment, one accent color, or a plainer sentence could do.

Two sensibilities exist in the code today. The light one — the home screen, the data table, the chart options list, the notes pane — is the style. The boxy one — bordered boxes around every form field, bordered buttons, nested frames — is legacy, and every migration moves a screen from the second to the first. Never add new UI in the boxy style.

Hard rules

These are checkable, and CI or review should treat a violation as a defect:

  1. One border per surface. A modal, sidebar or overlay gets one rounded frame and one title. Nothing inside it gets another border — not fields, not lists, not buttons. Structure inside comes from alignment, section rules, and the accent.
  2. No bordered buttons. An action is a key, and the footer names it (Enter Apply Esc Cancel). There is nothing to Tab onto and "press".
  3. Every color is a ColorConfig slot. No Color:: literals in widgets or render code. The current row uses the theme's highlight_style helper; Modifier::REVERSED never appears outside it.
  4. Every glyph is a glyphs.rs slot with an ASCII twin. LANG=C must render every screen legibly. No emoji, no Nerd Font characters. Slots already exist for checkboxes, radios, dots, score marks, sort marks, ellipsis, middot, spinner, scrollbar; add a slot before adding a symbol.
  5. Esc discards, Enter applies. Esc closes a surface and its staged edits die with it; reopening shows applied state, never leftovers. Enter applies from anywhere in a form (a multiline field is the one exception, where Enter types and the footer says what applies).
  6. State is visible. Anything that changes what the table shows leaves a mark: sort direction on the header, Rows: 417 of 1,000 under a filter or query, the drill-down breadcrumb, the reshape chip. If a feature mutates the view invisibly, the feature is unfinished.
  7. The keybind contract (below) is frozen. Everything else about a screen may change.

Color

  • One accent (accent): keys in chips, focused titles and labels, the selection rail, the active tab. If two things on one screen compete for the accent, one of them is wrong. accent_bright never marks focus.
  • Three chrome tiers a few shades apart: controls_bg, table_header_bg, table_alternate_row. Backgrounds never carry meaning beyond these.
  • Column names take their type's color; nulls are ∅ in dimmed.
  • gradient_start/gradient_end color the wordmark only.
  • Errors use the error slots; warnings warning; everything informational is plain or dimmed. No new semantic colors without a config slot and a dark/light default pair.

Text

  • Titles: unpadded Title Case (Sort & Filter, Pivot & Melt). Never SCREAMING, never key hints inside a title.
  • Field labels: sentence case with a colon (X axis:, Log scale:). Spelled out — no Col/Op/Val abbreviations.
  • Chip labels: sentence case, short (Open all, Copy row); a feature's own name keeps its capitals (Sort & Filter).
  • Key spelling: ^X in chips; Ctrl+X in help text and prose. Arrows are glyphs (↑↓, with ASCII twins), never the words "Up/Down".
  • American English. No filler ("Please…", "Note that…"). A label is a noun or a verb, not a sentence; a status line is one sentence, not a paragraph.
  • Ellipsis in status lines is ASCII ...; the ellipsis glyph is only a truncation marker inside content.

Components

Build these once in widgets/ui/ and reuse them everywhere; a screen that hand-rolls one of these is a migration target.

Surface — the one border. Rounded, Title Case title on the frame, optional one-line footer of chips, with a blank row between it and the content so a focused field's accent never reads as part of a chip (#650). Confirm/Success/Error, every modal, every sidebar, the help overlay: all Surfaces.

FormRow — label value on one line inside a Surface, behind a reserved one-column rail gutter. The focused row carries the ▎ rail and its label in the accent plus the cursor; unfocused rows stay plain, and the chosen value is always echoed so nothing is ambiguous when focus is elsewhere. The gutter is always there, so focus arriving moves nothing — Tab walks the rail down the rows, which is what tells a first session the rows are walkable. Variants: text, number (←/→ or +/- adjust), toggle (checkbox glyph), and picker (opens a Picker; the row shows the current choice).

╭Pivot & Melt─────────────────────────────╮
│ Pivot │ Melt                            │
│                                         │
│  Index        department                │
│  Columns      job_title                 │
│ ▎Values       salary                    │   ← focused row: rail, accent
│  Aggregate    avg                       │      label, value under edit
│                                         │
│ department × job_title → avg(salary)    │   ← the spec, echoed in full
│ Enter Apply   Tab Next   Esc Cancel     │   ← HintBar, chips
╰─────────────────────────────────────────╯

Picker — the type-to-narrow list (the Sort tab already has the right one; extract it). Type filters, ↑↓ move, Enter chooses and returns to the row. Used for columns, operators, formats, aggregations, sheets. A radio group is a short Picker, not a grid: arrow-navigation across a 4×2 grid is how the wrong aggregation gets exported. The selection carries the rail and the tint while the list is focused, and only the accent when it is not: inside one Surface the rail means focus, and Tab visibly moves it.

Confirm — one question, one component: confirmation_modal (render/overlays.rs), with a pending_* field naming what Yes does. The choices are named (Overwrite, Delete, Run), the rail marks the one Enter takes, and a destructive one opens on No (show_destructive). Its own footer (Enter Confirm ←→ Switch Esc Cancel); ↑↓ and j/k scroll a long question. Never a feature's own popup with its own keys. The error modal is its sibling: the message and Enter Close, nothing else.

HintBar — the chip row of every Surface footer. Primary action first, Esc last; only keys that work right now.

Status footer — render/footer.rs, the bottom of every screen: a thin rule in table_column_separator, then one line with no fill.

  • Left, in pipeline order: dataset › query (the label only; : shows the text) › other stages (pivoted, not the Delta table) › filters · sort (prcp > 0 · date ▼), then background work behind its spinner and quiet facts (following · 3s ago, find "x" · match 3). Separators are the trail and middot glyphs.
  • Right: the position (41,208 / 1,204,331, led by col 3/40 on a wide table), the mode's keys, then ? keys (F1 keys where ? types). Hints are quiet: the key in the accent, bold, the label plain; no chip fill.
  • At rest only ? keys. A mode offers its two or three keys only while it is active, labels read from the key registry (registry_hint): column cursor moved → +/- Filter [/] Sort F Counts; find in effect → n/N Next Esc Clear; a wait Esc stops → Esc Stop; a by view → Enter Drill. A screen's own (analysis, chart, value counts, hex) are its first three, Esc kept. A surface with its own footer (dialog, sidebar, inspector) adds none.
  • When the line is short, segments yield in a fixed order (Footer::fit): the dataset's name (middle-elided, and the first to go for a message), filters/sort (counted: 2 filters · sorted), notes, background work (its spinner alone), stages, the position (41,208 / 1.2M, then gone), the message (cut), ? keys → ?, then the mode's keys from the right; help never goes.
  • A message (flash) takes room from those segments; it never adds a line. Errors stay in the error modal.
  • The footer grows, at most three lines, only for something ongoing: a prompt being typed (find, the command line: render/input_strip.rs) or a job with counts (ProgressLine: counts done / total, a bar when a total is known, Esc Stop when it stops). It grows upward, taking table rows from the bottom; the view's top stays put. A framed takeover (the inspector) needs no rule: its border sets it off.
  • Every footer key is a click target (Footer::render_line returns where each landed).

Section rule — the home screen's TITLE ── count line. The way to divide space inside a Surface without borders. A rule never signals focus: same weight and color whether focus is in its section or not.

Plot axes — widgets/axes.rs, shared by the chart view and the Distribution plots. Ticks at round values (1-2-5 steps, calendar boundaries on a time axis), about one label per 15 columns and one per 4 rows; labels never touch, and a crowded axis takes a coarser step before a shorter form. Tick marks and the grid are PlotMarks slots with ASCII twins. The grid is off until asked for, sits under the series in chart_grid (a shade under dimmed, and a color rather than a grey so 16 colors keep it off the background), and never takes a cell a series drew in; a legend names two or more series, unframed (a swatch and a name on the cleared background), where it covers the fewest marks. The line and scatter crosshair (x, or a click) takes the keys from the panel: its line is in the accent, under the series, and its readout sits under the plot. Over the plot one title row, always kept: how the chart is made (mean by month, colored by carrier) in text_secondary, and at its right what the chart read (sample of 10,000 of 337k rows) in dimmed, cut first. A column is named at its axis (Y over the y labels, X at the right under the x labels), never in the title; the export is laid out the same way.

Shelves — the chart panel (widgets/chart.rs): the same rows for every chart type, label value with the line under a shelf (bucket, order, bins, which values) indented to the value column, then an Options section rule. A setting that reads as a value without its name gets its own labeled row (Aggregate under Y: unlabeled, none read as a second Y column; #757). A shelf the type does not use stays where it is, dimmed with a word of why (density, same as X), and Tab skips it: nothing moves when the type changes. Column names take their type's color. A picker drops over the panel as a Surface under the row it edits.

Shapes

Four shapes, chosen by what the user needs to keep seeing:

  • Sidebar (right, data stays visible): iterative controls whose effect you watch — Sort & Filter, chart options, views.
  • Centered dialog (small): a commitment — confirm, export, errors.
  • Takeover (full screen): a different way of looking — analysis, chart canvas, the Pivot & Melt builder (form beside a live preview of the result, stacked when narrow; a preview runs in the background on a few rows, never in render).
  • Footer prompt (bottom): text entry — the command line, find. It lives in the status footer, which grows a line for it.

A feature gets one shape. Hints, tabs, focus and footers work identically across shapes.

Focus

One signal: the focused element carries the accent (title, label, or rail). Focus never silently jumps (the analysis screen's jump-to-results is a defect, not a pattern). Selection that is not focused stays visible, dimmed.

  • One rail on screen. A FormRow whose picker is open gives its rail to the picker's current line (FormRow::render_picking) and keeps its accent label; an inline editor does the same while its picker is open.
  • Rules never signal focus: no heavy rule, no brighter title.
  • No accent_bright for focus: the confirm choice, a focused label and a focused title all use accent.

Forms: one set of keys

Every form, dialog and option sidebar is a crate::form::Form: an ordered list of fields, each a FieldKind (Text, MultilineText, Choice, Checkbox, Picker, Button). form::key decides what a key means and moves focus; form::picker_key does the same for an open picker. The modal only says what a submit, a step or an action does. Never hand-roll focus enums' next/prev or per-modal arrow handling.

KeyDoes
Tab / Shift+Tab, ↓ / ↑Next / previous field, wrapping, from the moment the form opens
← / →Step a Choice (or a pick-one Picker's value); flip a Checkbox; cursor in Text
SpaceCheckbox: toggle. Choice: next value, wrapping. Picker: open. Button: do it
EnterSubmit from any field (MultilineText types it; Ctrl+J submits). Picker: choose
EscClose an open picker, otherwise cancel the form
Ctrl+P / Ctrl+NHistory in a Text field; ↑ ↓ never recall in a form
  • h j k l are the arrows on a field that does not type; letters never open a picker (Space does, then typing narrows).
  • In an open picker, Enter chooses. Space toggles in a list of several; in a pick-one list it chooses only while nothing is typed, then types a space, so a name of several words narrows (picker_key).
  • A hidden or disabled field is left out of fields(), so focus skips it; settle_focus after a change that hides the focused one.
  • A tab bar is the form's first field, a Choice: ←/→ switch tabs there, and only there. A viewer with no fields (the Info panel) switches tabs with ←/→ and Tab from anywhere instead.
  • Short enumerations are Choices; long lists (columns) are Pickers.
  • A form that applies live (the chart options) treats Enter as Space.
  • Mouse: a click focuses through Form::focus(field), then acts as Space (see Mouse below).

Mouse

The mouse is a shortcut to keys, never a second interface (pointer.rs).

  • Every action is a key. A click or drag resolves to the keys it stands for and goes through the pump's classify: it acts where those keys would act at once and is dropped where they would wait. Mouse input is never held. A new mouse action names its key equivalent and gates on it.
  • Record where you draw. A widget that lays out something clickable records its rect while drawing (pointer::record, record_field::<Form>, record_spans for tab lines); nothing recomputes layout for hit-testing. A new form row calls record_field beside its FormRow::render; a list from PickerState records itself, and a short choice's values side by side (FormValue::Options with clicks) record a click per value.
  • Forms: click = focus + Space (checkbox flips, choice steps, picker opens, button runs, text takes the cursor); right click on a choice = ←, on anything else focus only. A list row (a sort, a filter, a column in Sort & Filter; a form opts in with Form::list_row) is picked, not set: the first click only focuses it, a click on it focused acts. Settings act on the first click, as a checkbox is expected to. A tab is a Hit::Option on the tab bar field (←→ to it) or a Hit::Key when tabs have number keys. While a picker or an inline editor is open only its lines take clicks.
  • Click outside a dialog does nothing. Over help, an error or a question, only footer keys take clicks.
  • Table: click a cell or header = cursor; drag a header = H/L (drop mark: the rule glyph in the accent on the header; off the columns, or any key, puts it back); drag the gap after a header, or the last header cell of a column cut at the right side, = </> width, MIN_WIDTH..MAX_WIDTH. Drags are coalesced.
  • Context menu (right click on a cell): a Surface at the pointer, clamped on screen, one line per key with the key shown; Enter or a click presses the line's key as typed (AppEvent::Press). It opens only where the cursor landed, and its own keys act while busy. It is not a dialog: a click elsewhere closes it, and any other key closes it and acts.
  • Footer: every hint, ? keys, the filters/sort (s) and query (:) are click targets (Drawn), only where those keys mean that.
  • display.mouse = false / --mouse=false leaves the mouse to the terminal; Shift+drag selects text in most terminals. Docs: docs/user-guide/mouse.md.

Feedback

Every message earns its interruption level. The channel is chosen by what the user must do about the information, never by which feature sent it:

  1. State gets a mark, not a message. Anything that stays true while it holds — sort direction, the row count, the reshape chip, the drill breadcrumb — lives on screen as state (hard rule 6) and is never re-announced in a message.
  2. Completions get a flash. An action that finished and needs no decision — a copy, an export, a saved view — shows one plain sentence in the footer's status line, without a spinner, cleared after about two seconds or on the next keypress, whichever comes first. Appearing and expiring move nothing to the right of it: the flash takes the dataset's name's room first, never the mode's keys.
  3. Validation stays inline. A form that cannot apply says why on its own status line inside the Surface, warning at most. Enter on an invalid form re-accents that line; it never raises a modal.
  4. Only a failure that stops the user gets a modal — a load that failed, an auth that was refused. Acknowledge to continue. The modal is a Surface like any other.
  5. A failed write keeps its form. An export that could not write brings its dialog back as it was, every value intact, the reason on the status line: the fix is typed where the reason is read. The form is suspended while it writes, never closed until the write lands.

One flash component serves every screen: the home status line is the same component, not a sibling, so duration, clearing and styling cannot drift. The success modal is retired — any use of it that fits rung 2 becomes a flash. Non-critical background errors (cache, history) keep logging and continuing; silence is the right channel for them.

Every terminal, every size

datui runs on a truecolor desktop terminal, over SSH with a C locale and a bitmap font, maximized on an ultrawide, and in a 60-column strip parked in the corner of someone's screen. A screen is not done until it works in all four, and neither extreme is the one that suffers.

Capability tiers. Four, degrading independently: UTF-8 → ASCII (the glyphs.rs twin sets), and truecolor → 256 → 16-color ANSI (ColorConfig does the mapping). Every screen must be legible at the floor of both — check with LANG=C and with TERM forced to a 16-color terminal. Meaning may never live only in a glyph or only in a color: the ASCII twin carries the same distinction, and a 16-color palette still separates accent, dimmed and error. Never assume the font: no Nerd Font glyphs, and a new Unicode character must pass the coverage audit (scripts/code/audit_glyphs.py, documented in docs/for-developers/glyph-audit.md) — present in JetBrainsMono Nerd Font, Liberation Mono and Noto Sans Mono, and never an Emoji=Yes, Emoji_Presentation=No codepoint those fonts don't all carry, because a missing one falls back to the color emoji font and renders a blank cell (#325 has the full rule and history). Richer glyphs are the user's [glyphs] config overrides, never the defaults.

Small windows. The baseline is full usability at 80×24, and graceful loss down to roughly 60×20. When width or height runs out, elements yield in reverse order of importance: branding first (the home wordmark already steps down to the one-line title on short or narrow terminals), then conveniences, then primary actions; the way out (Esc/quit chip) goes last, the data never — which is why the footer gives up its segments in a fixed order and keeps the mode's keys and help to the end. Sidebars cap their share of the width and collapse before the table does; overlays scroll inside a capped frame rather than growing past the screen; nothing ever wraps a table row. When height runs out, footers and headers stay, content scrolls, and partial items are counted ("… 3 more") rather than half-drawn.

Ultrawides. The failure mode is distance, not space. Facts a decision needs must sit next to the thing decided about — the locality marker lives beside the row's name, not only in a details pane a foot to the right, for exactly this reason. Reading surfaces (help, notes, detail panes) cap their line length at a comfortable measure instead of stretching; tables may use the width, prose may not. Centered dialogs stay compact rather than scaling with the terminal.

No jitter. Labels arriving asynchronously, spinner frames, and count updates must not move anything around them: equal-width frames, reserved columns, and same-width glyph pairs are the rule everywhere something updates in place.

Keybind compatibility contract

Frozen — users may be retrained on form internals, never on moving and leaving:

  • Arrows and h/j/k/l; PgUp/PgDn (and Ctrl+F/B, Ctrl+D/U at the table); Home/End (G); : and digits go to a row (the command line, row:). At the table h/l and ←/→ move the column cursor, and the columns scroll only when it would leave the screen; Shift+←/→, { } and g move it too (#574 made the change; before, h/l scrolled the view a column). [ ] sort by the cursor's column (the UX sprint moved paging to Shift+←/→ alone).
  • Esc's layered back-out; q pops to home when the dataset was opened from it and quits otherwise (help says which); Ctrl+Q quits and Ctrl+C from anywhere, a text field included (#649; a field copies with Alt+W); Q quits at the table and during a load, and is an ordinary key inside other surfaces (chart deliberately has no quit key); Ctrl+O home. (#320 landed the approved q evolution.)
  • ? and F1 for help, including home's empty-filter ?.
  • The feature keys: / f n N s c a p e i v V r R # F , D H L + - [ ] (/ and f find; : is the command line: a row, SQL or q), Enter-to-drill (where there is no group to drill into, Enter inspects the row, as Space does; #542). (#317 moved the views list from t/T to v/V with the rename; #559 moved row numbers from N to # for find's f, n and N; #558 gave F to value counts and moved digit grouping to ,; #737 gave H/L to moving the cursor's column and +/- to filtering on its cell, and moved the CSV header toggle to H on the Info panel's Schema tab; the UX sprint made / find and : the command line, folded the query's Text mode into find's Ctrl+G, and gave [/] to sorting.)
  • Text fields keep their readline bindings.
  • Home's type-to-filter: every printable except ? and Space (empty filter only) and ~ goes to the filter. Never assign a letter key on the home screen.

Any other key may move, with its key registry entry and a release-notes line updated in the same PR. The docs' key tables are generated from the help strings: run cargo run -p datui-cli --bin gen_docs -- write.

Acceptance checklist for a migrated screen

  • One border on the surface; zero inside it; no bordered buttons.
  • Focused element accented; selected-but-unfocused still visible.
  • All local keys in a HintBar footer, chip grammar, primary first.
  • No Color:: literals; no REVERSED outside the theme helper; every glyph from glyphs.rs; LANG=C screenshot is clean.
  • Usable at 80×24 and degrades sanely to ~60×20; reading surfaces cap their measure on wide terminals; nothing jitters as labels arrive.
  • Esc discards; Enter applies; reopening shows applied state.
  • View-changing state visible on the main screen after the surface closes.
  • Contract keybinds identical before and after.
  • Help string and docs page updated, gen_docs write run (keyboard-shortcuts.md is generated from the key registry); buffer test for the layout; integration test for the keys.
  • Every code block in the docs page is runnable and passes scripts/docs/doc_examples.py, or is labeled ,template with <PLACEHOLDER>s; the page leads with the key, then a table.
  • Before/after screenshots in the PR.

Anti-pattern gallery (what the migrations delete)

  • A bordered box per form field, label as box title (the old template form).
  • Bordered Save/Cancel buttons reached by Tab.
  • Key hints inside a title (ACCESS PLAN — Esc Close).
  • A 4×2 radio grid navigated by arrows.
  • A filter box that silently filters other lists than the one it sits on.
  • Modifier::REVERSED as a tab highlight in one screen and BOLD in another.
  • A dataset mutated with nothing on screen saying so.
  • A feature's own confirmation popup, or one that acts on Enter at once.
  • Two rails on screen: a row's and its open picker's.
  • A heavy or brighter section rule to say where focus is.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.