Import beads' UX design system into gastown: - Add internal/ui/ package with Ayu theme colors and semantic styling - styles.go: AdaptiveColor definitions for light/dark mode - terminal.go: TTY detection, NO_COLOR/CLICOLOR support - markdown.go: Glamour rendering with agent mode bypass - pager.go: Smart paging with GT_PAGER support - Add colorized help output (internal/cmd/help.go) - Group headers in accent color - Command names styled for scannability - Flag types and defaults muted - Add gt thanks command (internal/cmd/thanks.go) - Contributor display with same logic as bd thanks - Styled with Ayu theme colors - Update gt doctor to match bd doctor UX - Category grouping (Core, Infrastructure, Rig, Patrol, etc.) - Semantic icons (✓ ⚠ ✖) with Ayu colors - Tree connectors for detail lines - Summary line with pass/warn/fail counts - Warnings section at end with numbered issues - Migrate existing styles to use ui package - internal/style/style.go uses ui.ColorPass etc. - internal/tui/feed/styles.go uses ui package colors Co-Authored-By: SageOx <ox@sageox.ai>
107 lines
2.3 KiB
Go
107 lines
2.3 KiB
Go
package ui
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
// PagerOptions configures pager behavior for command output.
|
|
type PagerOptions struct {
|
|
// NoPager disables pager for this command (--no-pager flag)
|
|
NoPager bool
|
|
}
|
|
|
|
// shouldUsePager determines if output should be piped to a pager.
|
|
// Returns false if explicitly disabled, env var set, or stdout is not a TTY.
|
|
func shouldUsePager(opts PagerOptions) bool {
|
|
if opts.NoPager {
|
|
return false
|
|
}
|
|
if os.Getenv("GT_NO_PAGER") != "" {
|
|
return false
|
|
}
|
|
if !term.IsTerminal(int(os.Stdout.Fd())) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// getPagerCommand returns the pager command to use.
|
|
// Checks GT_PAGER, then PAGER, defaults to "less".
|
|
func getPagerCommand() string {
|
|
if pager := os.Getenv("GT_PAGER"); pager != "" {
|
|
return pager
|
|
}
|
|
if pager := os.Getenv("PAGER"); pager != "" {
|
|
return pager
|
|
}
|
|
return "less"
|
|
}
|
|
|
|
// getTerminalHeight returns the terminal height in lines.
|
|
// Returns 0 if unable to determine (not a TTY).
|
|
func getTerminalHeight() int {
|
|
fd := int(os.Stdout.Fd())
|
|
if !term.IsTerminal(fd) {
|
|
return 0
|
|
}
|
|
_, height, err := term.GetSize(fd)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return height
|
|
}
|
|
|
|
// contentHeight counts the number of lines in content.
|
|
// Returns 0 if content is empty.
|
|
func contentHeight(content string) int {
|
|
if content == "" {
|
|
return 0
|
|
}
|
|
return strings.Count(content, "\n") + 1
|
|
}
|
|
|
|
// ToPager pipes content to a pager if appropriate.
|
|
// Prints directly if pager is disabled, stdout is not a TTY, or content fits in terminal.
|
|
func ToPager(content string, opts PagerOptions) error {
|
|
if !shouldUsePager(opts) {
|
|
fmt.Print(content)
|
|
return nil
|
|
}
|
|
|
|
termHeight := getTerminalHeight()
|
|
lines := contentHeight(content)
|
|
|
|
// print directly if content fits in terminal (leave room for prompt)
|
|
if termHeight > 0 && lines <= termHeight-1 {
|
|
fmt.Print(content)
|
|
return nil
|
|
}
|
|
|
|
pagerCmd := getPagerCommand()
|
|
parts := strings.Fields(pagerCmd)
|
|
if len(parts) == 0 {
|
|
fmt.Print(content)
|
|
return nil
|
|
}
|
|
|
|
cmd := exec.Command(parts[0], parts[1:]...)
|
|
cmd.Stdin = strings.NewReader(content)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
// set LESS options if not already configured
|
|
// -R: allow ANSI color codes
|
|
// -F: quit if content fits on one screen
|
|
// -X: don't clear screen on exit
|
|
if os.Getenv("LESS") == "" {
|
|
cmd.Env = append(os.Environ(), "LESS=-RFX")
|
|
}
|
|
|
|
return cmd.Run()
|
|
}
|