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.
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 afterSearch for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer