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
- lxsmnsyc/agent-skillstheir own version, first seen Nov 8, 2026
- getvibekits/vibe-coding-rulestheir own version, first seen Oct 25, 2026
- mia-cx/skillstheir own version, first seen Oct 16, 2026
- boris410/claude-skillstheir own version, first seen Oct 11, 2026
Other versions4 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 4
- lxsmnsyc/agent-skills★ 4same name
- mia-cx/skills★ 4same name
- boris410/claude-skills★ 0same name
- 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.
- AGENTS.mdAGENTS.md
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
- Very specific: line 144
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 …
- Very specific: line 157
Every footer key is a click target (Footer::render_line returns where each landed).
- Distinctive: line 218
Rules never signal focus: no heavy rule, no brighter title.
- Very specific: line 224
Every form, dialog and option sidebar is a crate::form::Form: an ordered list of fields, each a FieldKind (Text, MultilineText, Choice, …
- Very specific: line 290
Footer: every hint, ? keys, the filters/sort (s) and query (:) are click targets (Drawn), only where those keys mean that.
- Distinctive: line 298
Every message earns its interruption level.
“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.
Files
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:
- 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.
- 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". - Every color is a
ColorConfigslot. NoColor::literals in widgets or render code. The current row uses the theme'shighlight_stylehelper;Modifier::REVERSEDnever appears outside it. - Every glyph is a
glyphs.rsslot with an ASCII twin.LANG=Cmust 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. - 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).
- State is visible. Anything that changes what the table shows leaves a
mark: sort direction on the header,
Rows: 417 of 1,000under a filter or query, the drill-down breadcrumb, the reshape chip. If a feature mutates the view invisibly, the feature is unfinished. - 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_brightnever 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
∅indimmed. gradient_start/gradient_endcolor the wordmark only.- Errors use the error slots; warnings
warning; everything informational is plain ordimmed. 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 — noCol/Op/Valabbreviations. - Chip labels: sentence case, short (
Open all,Copy row); a feature's own name keeps its capitals (Sort & Filter). - Key spelling:
^Xin chips;Ctrl+Xin 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
...; theellipsisglyph 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 thetrailandmiddotglyphs. - Right: the position (
41,208 / 1,204,331, led bycol 3/40on a wide table), the mode's keys, then? keys(F1 keyswhere?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; abyview →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 Stopwhen 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_linereturns 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_brightfor focus: the confirm choice, a focused label and a focused title all useaccent.
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.
| Key | Does |
| 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 |
| Space | Checkbox: toggle. Choice: next value, wrapping. Picker: open. Button: do it |
| Enter | Submit from any field (MultilineText types it; Ctrl+J submits). Picker: choose |
| Esc | Close an open picker, otherwise cancel the form |
| Ctrl+P / Ctrl+N | History in a Text field; ↑ ↓ never recall in a form |
h j k lare 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_focusafter 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_spansfor tab lines); nothing recomputes layout for hit-testing. A new form row callsrecord_fieldbeside itsFormRow::render; a list fromPickerStaterecords itself, and a short choice's values side by side (FormValue::Optionswithclicks) 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 aHit::Optionon the tab bar field (←→ to it) or aHit::Keywhen 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: theruleglyph 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) andquery(:) are click targets (Drawn), only where those keys mean that. display.mouse = false/--mouse=falseleaves 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:
- 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.
- 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.
- Validation stays inline. A form that cannot apply says why on its
own status line inside the Surface,
warningat most. Enter on an invalid form re-accents that line; it never raises a modal. - 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.
- 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(andCtrl+F/B,Ctrl+D/Uat the table);Home/End(G);:and digits go to a row (the command line,row:). At the tableh/land←/→move the column cursor, and the columns scroll only when it would leave the screen;Shift+←/→,{ }andgmove it too (#574 made the change; before,h/lscrolled the view a column).[ ]sort by the cursor's column (the UX sprint moved paging to Shift+←/→ alone). - Esc's layered back-out;
qpops to home when the dataset was opened from it and quits otherwise (help says which);Ctrl+Qquits andCtrl+Cfrom anywhere, a text field included (#649; a field copies withAlt+W);Qquits at the table and during a load, and is an ordinary key inside other surfaces (chart deliberately has no quit key);Ctrl+Ohome. (#320 landed the approvedqevolution.) ?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 + - [ ](/andffind;: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 fromt/Ttov/Vwith the rename; #559 moved row numbers fromNto#for find'sf,nandN; #558 gaveFto value counts and moved digit grouping to,; #737 gaveH/Lto moving the cursor's column and+/-to filtering on its cell, and moved the CSV header toggle toHon 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; noREVERSEDoutside the theme helper; every glyph fromglyphs.rs;LANG=Cscreenshot 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 writerun (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,templatewith<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/Cancelbuttons 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::REVERSEDas 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.
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
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.

