output.go

 1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
 2//
 3// SPDX-License-Identifier: AGPL-3.0-or-later
 4
 5// Package ui provides terminal output utilities and styles.
 6package ui
 7
 8import (
 9	"os"
10	"strings"
11
12	"github.com/mattn/go-isatty"
13)
14
15// ColorMode represents the color output mode.
16type ColorMode int
17
18// Color mode constants controlling terminal output styling.
19const (
20	ColorAuto   ColorMode = iota // Detect from environment/TTY
21	ColorAlways                  // Force colors on
22	ColorNever                   // Force colors off
23)
24
25//nolint:gochecknoglobals // intentional global for output mode
26var colorMode = ColorAuto
27
28// SetColorMode sets the global color mode.
29func SetColorMode(mode ColorMode) {
30	colorMode = mode
31}
32
33// IsPlain returns true when output should be unstyled plain text.
34// Detection priority:
35//  1. Explicit ColorNever/ColorAlways mode
36//  2. NO_COLOR env var (non-empty = plain)
37//  3. FORCE_COLOR env var (non-empty = styled)
38//  4. TERM=dumb (plain)
39//  5. TTY detection (non-TTY = plain)
40func IsPlain() bool {
41	switch colorMode {
42	case ColorNever:
43		return true
44	case ColorAlways:
45		return false
46	case ColorAuto:
47		return detectPlain()
48	}
49
50	return detectPlain()
51}
52
53// IsInteractive returns true when running in an interactive terminal.
54// This checks TTY status regardless of color settings.
55func IsInteractive() bool {
56	return isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd())
57}
58
59func detectPlain() bool {
60	// NO_COLOR takes precedence (https://no-color.org/)
61	if noColor := os.Getenv("NO_COLOR"); noColor != "" {
62		return true
63	}
64
65	// FORCE_COLOR overrides TTY detection
66	if forceColor := os.Getenv("FORCE_COLOR"); forceColor != "" {
67		// FORCE_COLOR=0 or FORCE_COLOR=false means no color
68		lower := strings.ToLower(forceColor)
69		if lower == "0" || lower == "false" {
70			return true
71		}
72
73		return false
74	}
75
76	// TERM=dumb indicates minimal terminal
77	if term := os.Getenv("TERM"); term == "dumb" {
78		return true
79	}
80
81	// Fall back to TTY detection
82	return !IsInteractive()
83}