output.go

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