skills/tui-design/SKILL.md
Design specification for CLI TUI (Terminal User Interface). This skill provides comprehensive guidelines for implementing interactive terminal UI components, including page layout structure, color schemes, keyboard navigation, and multi-level navigation principles.
npx skillsauth add castle-x/skills-x tui-designInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This document provides comprehensive design guidelines for implementing interactive terminal user interface (TUI) applications.
Every TUI page follows a consistent four-section layout:
┌────────────────────────────────────────────────────────────────┐
│ Header: Title Line + Separator + ASCII Logo + Separator │
├────────────────────────────────────────────────────────────────┤
│ Info Bar: Context-dependent information (Dynamic) │
├────────────────────────────────────────────────────────────────┤
│ Content Area: Interactive options/list (Scrollable) │
│ │
├────────────────────────────────────────────────────────────────┤
│ Footer: Status Bar + Separator + Keyboard Hints │
└────────────────────────────────────────────────────────────────┘
The header consists of a title line, separator, ASCII logo, and separator with symmetric spacing:
🚀 Skills-X - AI Agent Skills Manager v0.2.12
────────────────────────────────────────────────────────────
<- 1 blank line (from logo constant leading \n)
███████╗██╗ ██╗██╗██╗ ...
╚══════╝╚═╝ ╚═╝╚═╝╚══════╝...
<- 1 blank line (from logo constant trailing \n + 1 extra \n)
────────────────────────────────────────────────────────────
Spacing rules:
logo constant uses backtick string, which naturally includes a leading \n (after opening backtick) and trailing \n (before closing backtick)logoStyle.Render(logo), add exactly one \n — this produces symmetric 1-blank-line padding on both sides\n\n after logo — it creates asymmetric bottom paddingfunc RenderLogo(version string) string {
var b strings.Builder
// Title + version (strip -dirty suffix for display)
b.WriteString(accentStyle.Render("🚀 Skills-X - AI Agent Skills Manager"))
if version != "" {
displayVersion := strings.TrimSuffix(version, "-dirty")
b.WriteString(" " + hintStyle.Render(displayVersion))
}
b.WriteString("\n")
// Upper separator
b.WriteString(separatorStyle.Render(strings.Repeat("─", SeparatorWidth)))
b.WriteString("\n")
// Logo (constant has leading \n and trailing \n built-in)
b.WriteString(logoStyle.Render(logo))
b.WriteString("\n") // exactly 1 \n — symmetric with leading \n
// Lower separator
b.WriteString(separatorStyle.Render(strings.Repeat("─", SeparatorWidth)))
b.WriteString("\n")
return b.String()
}
Newline budget between header and content:
RenderLogo ends with separator + \n\n after RenderLogo() — content starts immediatelyKey features:
fmt.Sprintf("%4d")(共 30 个,已安装 12 个,将安装 3 个,将卸载 1 个)
────────────────────────────────────────────────────────────
/ 搜索 | 空格 选择/反选 | A 全选 | Enter 确认安装/卸载 | b 返回 | q 退出
Composition:
\n + counts in dim color, parenthesized\n + 60-char ─ line + \nfunc RenderStatusBar(total, installed, newSelected, deselected int) string {
return "\n" + hintStyle.Render(
fmt.Sprintf("(共 %d 个,已安装 %d 个,将安装 %d 个,将卸载 %d 个)",
total, installed, newSelected, deselected))
}
func RenderHint(hint string) string {
return "\n" + RenderSeparator() + hintStyle.Render(hint)
}
func RenderSeparator() string {
return separatorStyle.Render(strings.Repeat("─", SeparatorWidth)) + "\n"
}
| Color Name | Hex Code | Variable | Usage |
|------------|-----------|------------------|--------------------------|
| Primary | #00D4AA | primaryColor | Selected items, logo |
| Secondary | #FF6B6B | secondaryColor | Cursor, errors, alerts |
| Accent | #4ECDC4 | accentColor | Title line, highlights |
| Dim | #666666 | dimColor | Hints, separators |
| White | #FFFFFF | whiteColor | Section titles, headers |
| Yellow | #FFCC00 | yellowColor | Warnings (reserved) |
| Blue | #5599FF | blueColor | Links (reserved) |
| Cyan | #5A9FB8 | cyanColor | Selectable items |
logoStyle = lipgloss.NewStyle().Foreground(primaryColor).Bold(true)
titleStyle = lipgloss.NewStyle().Foreground(whiteColor).Bold(true)
selectedStyle = lipgloss.NewStyle().Foreground(primaryColor).Bold(true)
normalStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#AAAAAA"))
selectableStyle = lipgloss.NewStyle().Foreground(cyanColor)
hintStyle = lipgloss.NewStyle().Foreground(dimColor)
cursorStyle = lipgloss.NewStyle().Foreground(secondaryColor).Bold(true)
successStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#00FF00"))
separatorStyle = lipgloss.NewStyle().Foreground(dimColor)
searchStyle = lipgloss.NewStyle().Foreground(whiteColor).Background(lipgloss.Color("#333333"))
| Symbol | Meaning | Style |
|--------|-----------------|---------------|
| ☑ | Selected | selectedStyle |
| ☐ | Not selected | — |
| ❯ | Cursor position | cursorStyle |
fmt.Sprintf width specifiers count bytes, not terminal columns. This breaks alignment when:
Rule: Always pad plain text to the target visual width, then apply lipgloss style.
// termWidth returns the visual width of a string in terminal columns.
func termWidth(s string) int {
w := 0
for _, r := range s {
if isCJK(r) {
w += 2
} else {
w++
}
}
return w
}
// padRight pads a string with spaces to reach the target visual width.
func padRight(s string, targetWidth int) string {
w := termWidth(s)
for w < targetWidth {
s += " "
w++
}
return s
}
// Header: pad plain text, then style
headerName := padRight("AI 工具", 22) // "AI 工具" = 7 visual cols → 15 spaces added
b.WriteString(fmt.Sprintf(" %s %s %s\n",
titleStyle.Render(headerName), // styled AFTER padding
titleStyle.Render(padRight("全局", 4)), // "全局" = 4 visual cols → 0 spaces
titleStyle.Render(padRight("项目", 4))))
// Data rows: same column widths
name := padRight(p.Name, 22) // "Claude Code" = 11 cols → 11 spaces
globalStr := fmt.Sprintf("%4d", count) // right-aligned number, 4 chars
b.WriteString(fmt.Sprintf("%s%s %s %s\n",
prefix, // " " or "❯ " (always 2 visual cols)
style.Render(name), // styled AFTER padding
globalCountStyle.Render(globalStr), // styled AFTER formatting
projectCountStyle.Render(projectStr)))
| Column | Visual Width | Alignment | Padding Method |
|--------------|-------------|-----------|-------------------------------|
| Name | 22 | Left | padRight(text, 22) |
| Skill name | 40 | Left | padRight(text, 40) |
| Number | 4 | Right | fmt.Sprintf("%4d", n) |
| Status (✓/✗) | 1 | Center | Direct render |
| Prefix (❯) | 2 | Left | " " or cursorStyle("❯ ") |
Use 2 spaces (" ") between columns in fmt.Sprintf:
fmt.Sprintf("%s%s %s %s\n", prefix, name, col1, col2)
// ^^ ^^ two-space gaps
All separators use the same constant width:
const SeparatorWidth = 60
| Location | Character | Width | Color |
|----------------------|-----------|-------|----------|
| Above ASCII logo | ─ | 60 | dimColor |
| Below ASCII logo | ─ | 60 | dimColor |
| Below table header | ─ | 60 | dimColor |
| Above footer hints | ─ | 60 | dimColor |
All separators use SeparatorWidth (60). Never hardcode different widths.
| Key | Action |
|----------|------------------|
| q | Quit application |
| Ctrl+C | Force quit |
| Key | Action |
|----------|---------------------------------|
| ↑ | Move cursor up (wrap to bottom) |
| ↓ | Move cursor down (wrap to top) |
| PgUp | Page up |
| PgDown | Page down |
| Home | Jump to first item |
| End | Jump to last item |
| Key | Action |
|-----------|--------------------------------------|
| Space | Toggle selection (cursor stays) |
| a / A | Select all visible items |
| Enter | Confirm selection |
| Key | Action |
|--------------|-----------------------------|
| Esc / b | Go back to previous level |
| Enter | Proceed to next level |
| Key | Action |
|-----------------|-------------------------------|
| / / Ctrl+F | Enter search mode |
| Typing | Append to search query |
| Backspace | Remove last character |
| Esc | Exit search mode, clear query |
Level 1 Level 2 Level 3 Level 4
│ │ │ │
▼ ▼ ▼ ▼
Select ────────► Select ──────────► Select ──────────► Install
AI Tool Target (global/ Skills (multi- Progress
project) select + search)
Enter confirms and proceeds to next levelEsc or b returns to previous level (restarts the loop)q or Ctrl+C exits at any levelAlt screen is managed once at the top level to avoid flicker between pages:
func RunTUI(opts TUIOptions) error {
// Enter alt screen ONCE for the entire TUI session
fmt.Print(EnterAltScreen)
fmt.Print(HideCursor)
defer func() {
fmt.Print(ShowCursor)
fmt.Print(ExitAltScreen)
}()
return runTUIFlow(opts)
}
func runTUIFlow(opts TUIOptions) error {
// Level 1
fmt.Print(ClearScreen)
product, err := RunProductSelect(opts.Version, opts.TargetDir)
if err != nil || product == nil {
return err
}
// Level 2
fmt.Print(ClearScreen)
target, err := RunInstallTargetSelect(product, opts.TargetDir)
...
// Level 3
fmt.Print(ClearScreen)
selected, deselected, err := RunSkillsSelect(...)
if err != nil && err.Error() == "go back" {
return runTUIFlow(opts) // recursive restart
}
// Level 4
fmt.Print(ClearScreen)
RunInstaller(selected, deselected, targetDir)
// Exit alt screen to show final result on normal terminal
fmt.Print(ShowCursor)
fmt.Print(ExitAltScreen)
fmt.Printf("Completed: %d\n", completed)
return nil
}
The rendering uses a two-layer approach to eliminate flicker:
Layer 1: Alt screen lifecycle (managed by RunTUI)
ClearScreen between each page transition (within the alt screen buffer)tea.WithAltScreen() on individual programs — it causes exit/enter cycles that flash the normal terminalLayer 2: Per-page rendering (managed by Bubble Tea)
tea.NewProgram(model) (no WithAltScreen)View() returns complete page content; never include ClearScreen in View()// Individual programs: NO WithAltScreen
p := tea.NewProgram(model) // Bubble Tea renders incrementally
// Page transitions: ClearScreen within the shared alt screen
fmt.Print(ClearScreen) // clears alt screen buffer between pages
Anti-patterns:
tea.NewProgram(model, tea.WithAltScreen()) per level → flicker on every page switchClearScreen inside View() → full repaint on every keystrokefmt.Printf(...) between levels while in alt screen → cursor position corruptionconst ClearScreen = "\033[2J\033[H" // Clear screen + cursor home
const EnterAltScreen = "\033[?1049h" // Enter alternate screen buffer
const ExitAltScreen = "\033[?1049l" // Exit alternate screen buffer
const HideCursor = "\033[?25l" // Hide cursor
const ShowCursor = "\033[?25h" // Show cursor
🚀 Skills-X - AI Agent Skills Manager v0.2.12
────────────────────────────────────────────────────────────
███████╗██╗ ██╗██╗██╗ ██╗ ███████╗ ██╗ ██╗
...
╚══════╝╚═╝ ╚═╝╚═╝╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝
────────────────────────────────────────────────────────────
AI 工具 全局 项目
────────────────────────────────────────────────────────────
❯ Claude Code 45 0
Cursor 0 36
Windsurf 0 0
────────────────────────────────────────────────────────────
↑↓ 选择 | Enter 确定 | q 退出
🚀 Skills-X - AI Agent Skills Manager v0.2.12
────────────────────────────────────────────────────────────
███████╗...
╚══════╝...
────────────────────────────────────────────────────────────
选择要安装的 Skills Cursor
📁 /path/to/project
🔍 搜索技能...
已安装
────────────────────────────────────────────────────────────
❯ ☑ anthropic/brainstorming ✓
☑ anthropic/doc-coauthoring ✓
☐ anthropic/frontend-design ✗
...
↑ 3/30 ↓
(共 30 个,已安装 12 个,将安装 3 个,将卸载 1 个)
────────────────────────────────────────────────────────────
/ 搜索 | 空格 选择/反选 | A 全选 | Enter 确认安装/卸载 | b 返回 | q 退出
Installing Skills
Progress: [========================================] 100% (9/9)
Completed: 9 | Failed: 0
To uninstall:
[OK] anthropic/doc-coauthoring
[OK] anthropic/frontend-design
────────────────────────────────────────────────────────────
Done! 9 completed.
const SeparatorWidth = 60 // All separators use this width
const DefaultPageSize = 10 // Default items per page in scrollable lists
─ lines use SeparatorWidth (60), never hardcode other valuespadRight/termWidth for CJK-aware column alignment; pad plain text first, style afterdocumentation
Guide for contributing new skills to the skills-x collection. This skill should be used when users want to add new open-source skills from external sources (like agentskills.io or anthropics/skills) to the skills-x repository. It covers the complete workflow from discovery to publishing.
tools
Use when designing or refining UIs that must be visually minimal, low-noise, and icon-forward while staying understandable for new users, especially when reducing text, consolidating controls, or streamlining dialogs, toolbars, search panels, or list results.
development
Integrate PocketBase as a Go library using the github.com/castle-x/goutils/pocketbase (gopb) package to build single-binary full-stack applications. Use when building Go applications that need user authentication, embedding PocketBase into Go binary, registering custom API routes, managing default users, serving embedded SPA frontend, or deploying single-binary applications. NOT for using PocketBase as a standalone separate process.
tools
Use when defining or updating Go CLI i18n rules in this repo, especially around locale files, env-based language selection, and key naming.