Files
gastown/internal/ui/pager.go
Ryan Snodgrass e1f2bb8b4b feat(ui): import comprehensive UX system from beads
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>
2026-01-09 22:46:06 -08:00

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()
}