feat(ux): reorganize command groups and add Tufte-inspired help styling

- Add semantic coloring to help output (section headers, flags, types)
- Reorganize commands into clearer categories:
  - "Views & Reports" for readonly queries (ready, stats, blocked, etc.)
  - "Maintenance" for health/repair tasks (doctor, validate, migrate, etc.)
  - Merge "Issue Metadata" back into "Working With Issues"
- Highlight "doctor" as primary maintenance entry point with "(start here)"
- Create docs/UI_PHILOSOPHY.md documenting Tufte-inspired design principles
- Add guidance for PR to not include .beads/issues.jsonl
This commit is contained in:
Ryan Snodgrass
2025-12-20 12:57:16 -08:00
parent f6392efd07
commit fb1dff4f56
3 changed files with 206 additions and 0 deletions

View File

@@ -6,6 +6,7 @@ import (
"os"
"os/signal"
"path/filepath"
"regexp"
"runtime/pprof"
"runtime/trace"
"slices"
@@ -24,6 +25,7 @@ import (
"github.com/steveyegge/beads/internal/storage"
"github.com/steveyegge/beads/internal/storage/memory"
"github.com/steveyegge/beads/internal/storage/sqlite"
"github.com/steveyegge/beads/internal/ui"
"github.com/steveyegge/beads/internal/utils"
)
@@ -140,6 +142,148 @@ func init() {
// Add --version flag to root command (same behavior as version subcommand)
rootCmd.Flags().BoolP("version", "V", false, "Print version information")
// Command groups for organized help output (Tufte-inspired)
rootCmd.AddGroup(&cobra.Group{ID: "issues", Title: "Working With Issues:"})
rootCmd.AddGroup(&cobra.Group{ID: "views", Title: "Views & Reports:"})
rootCmd.AddGroup(&cobra.Group{ID: "deps", Title: "Dependencies & Structure:"})
rootCmd.AddGroup(&cobra.Group{ID: "sync", Title: "Sync & Data:"})
rootCmd.AddGroup(&cobra.Group{ID: "setup", Title: "Setup & Configuration:"})
// NOTE: Many maintenance commands (clean, cleanup, compact, validate, repair-deps)
// should eventually be consolidated into 'bd doctor' and 'bd doctor --fix' to simplify
// the user experience. The doctor command can detect issues and offer fixes interactively.
rootCmd.AddGroup(&cobra.Group{ID: "maint", Title: "Maintenance:"})
rootCmd.AddGroup(&cobra.Group{ID: "advanced", Title: "Integrations & Advanced:"})
// Custom help function with semantic coloring (Tufte-inspired)
// Note: Usage output (shown on errors) is not styled to avoid recursion issues
rootCmd.SetHelpFunc(colorizedHelpFunc)
}
// colorizedHelpFunc wraps Cobra's default help with semantic coloring
// Applies subtle accent color to group headers for visual hierarchy
func colorizedHelpFunc(cmd *cobra.Command, args []string) {
// Build full help output: Long description + Usage
var output strings.Builder
// Include Long description first (like Cobra's default help)
if cmd.Long != "" {
output.WriteString(cmd.Long)
output.WriteString("\n\n")
} else if cmd.Short != "" {
output.WriteString(cmd.Short)
output.WriteString("\n\n")
}
// Add the usage string which contains commands, flags, etc.
output.WriteString(cmd.UsageString())
// Apply semantic coloring
result := colorizeHelpOutput(output.String())
fmt.Print(result)
}
// colorizeHelpOutput applies semantic colors to help text
// - Group headers get accent color for visual hierarchy
// - Section headers (Examples:, Flags:) get accent color
// - Command names get subtle styling for scanability
// - Flag names get bold styling, types get muted
// - Default values get muted styling
func colorizeHelpOutput(help string) string {
// Match group header lines (e.g., "Working With Issues:")
// These are standalone lines ending with ":" and followed by commands
groupHeaderRE := regexp.MustCompile(`(?m)^([A-Z][A-Za-z &]+:)\s*$`)
result := groupHeaderRE.ReplaceAllStringFunc(help, func(match string) string {
// Trim whitespace, colorize, then restore
trimmed := strings.TrimSpace(match)
return ui.RenderAccent(trimmed)
})
// Match section headers in subcommand help (Examples:, Flags:, etc.)
sectionHeaderRE := regexp.MustCompile(`(?m)^(Examples|Flags|Usage|Global Flags|Aliases|Available Commands):`)
result = sectionHeaderRE.ReplaceAllStringFunc(result, func(match string) string {
return ui.RenderAccent(match)
})
// Match command lines: " command Description text"
// Commands are indented with 2 spaces, followed by spaces, then description
// Pattern matches: indent + command-name (with hyphens) + spacing + description
cmdLineRE := regexp.MustCompile(`(?m)^( )([a-z][a-z0-9]*(?:-[a-z0-9]+)*)(\s{2,})(.*)$`)
result = cmdLineRE.ReplaceAllStringFunc(result, func(match string) string {
parts := cmdLineRE.FindStringSubmatch(match)
if len(parts) != 5 {
return match
}
indent := parts[1]
cmdName := parts[2]
spacing := parts[3]
description := parts[4]
// Colorize command references in description (e.g., 'comments add')
description = colorizeCommandRefs(description)
// Highlight entry point hints (e.g., "(start here)")
description = highlightEntryPoints(description)
// Subtle styling on command name for scanability
return indent + ui.RenderCommand(cmdName) + spacing + description
})
// Match flag lines: " -f, --file string Description"
// Pattern: indent + flags + spacing + optional type + description
flagLineRE := regexp.MustCompile(`(?m)^(\s+)(-\w,\s+--[\w-]+|--[\w-]+)(\s+)(string|int|duration|bool)?(\s*.*)$`)
result = flagLineRE.ReplaceAllStringFunc(result, func(match string) string {
parts := flagLineRE.FindStringSubmatch(match)
if len(parts) < 6 {
return match
}
indent := parts[1]
flags := parts[2]
spacing := parts[3]
typeStr := parts[4]
desc := parts[5]
// Mute default values in description
desc = muteDefaults(desc)
if typeStr != "" {
return indent + ui.RenderCommand(flags) + spacing + ui.RenderMuted(typeStr) + desc
}
return indent + ui.RenderCommand(flags) + spacing + desc
})
return result
}
// muteDefaults applies muted styling to default value annotations
func muteDefaults(text string) string {
defaultRE := regexp.MustCompile(`(\(default[^)]*\))`)
return defaultRE.ReplaceAllStringFunc(text, func(match string) string {
return ui.RenderMuted(match)
})
}
// highlightEntryPoints applies accent styling to entry point hints like "(start here)"
func highlightEntryPoints(text string) string {
entryRE := regexp.MustCompile(`(\(start here\))`)
return entryRE.ReplaceAllStringFunc(text, func(match string) string {
return ui.RenderAccent(match)
})
}
// colorizeCommandRefs applies command styling to references in text
// Matches patterns like 'command name' or 'bd command'
func colorizeCommandRefs(text string) string {
// Match 'command words' in single quotes (e.g., 'comments add')
cmdRefRE := regexp.MustCompile(`'([a-z][a-z0-9 -]+)'`)
return cmdRefRE.ReplaceAllStringFunc(text, func(match string) string {
// Extract the command name without quotes
inner := match[1 : len(match)-1]
return "'" + ui.RenderCommand(inner) + "'"
})
}
var rootCmd = &cobra.Command{