changelog generator & diff tool
stormlightlabs.github.io/git-storm/
changelog
changeset
markdown
golang
git
1// package tty provides utilities for detecting terminal (TTY) availability and
2// generating appropriate fallback behavior for non-interactive environments.
3package tty
4
5import (
6 "errors"
7 "fmt"
8 "os"
9
10 "golang.org/x/term"
11)
12
13// IsTTY checks if the given file descriptor is a terminal.
14func IsTTY(fd uintptr) bool {
15 return term.IsTerminal(int(fd))
16}
17
18// IsInteractive checks if both stdin and stdout are connected to a terminal.
19// This is the primary check for determining if TUI applications can run.
20func IsInteractive() bool {
21 return IsTTY(os.Stdin.Fd()) && IsTTY(os.Stdout.Fd())
22}
23
24// IsCI detects if the current environment is a CI system by checking for common
25// CI environment variables.
26func IsCI() bool {
27 ciEnvVars := []string{
28 "CI", // Generic CI indicator
29 "CONTINUOUS_INTEGRATION",
30 "GITHUB_ACTIONS",
31 "GITLAB_CI",
32 "CIRCLECI",
33 "TRAVIS",
34 "JENKINS_URL",
35 "BUILDKITE",
36 "DRONE",
37 "TEAMCITY_VERSION",
38 }
39
40 for _, envVar := range ciEnvVars {
41 if os.Getenv(envVar) != "" {
42 return true
43 }
44 }
45
46 return false
47}
48
49// GetCIName attempts to identify the specific CI system being used.
50func GetCIName() string {
51 ciMap := map[string]string{
52 "GITHUB_ACTIONS": "GitHub Actions",
53 "GITLAB_CI": "GitLab CI",
54 "CIRCLECI": "CircleCI",
55 "TRAVIS": "Travis CI",
56 "JENKINS_URL": "Jenkins",
57 "BUILDKITE": "Buildkite",
58 "DRONE": "Drone CI",
59 "TEAMCITY_VERSION": "TeamCity",
60 }
61
62 for envVar, name := range ciMap {
63 if os.Getenv(envVar) != "" {
64 return name
65 }
66 }
67
68 if IsCI() {
69 return "CI"
70 }
71
72 return ""
73}
74
75// ErrorInteractiveRequired returns a formatted error message indicating that the
76// command requires an interactive terminal, with suggestions for alternatives.
77func ErrorInteractiveRequired(commandName string, alternatives []string) error {
78 msg := fmt.Sprintf("command '%s' requires an interactive terminal", commandName)
79
80 if IsCI() {
81 ciName := GetCIName()
82 msg += fmt.Sprintf(" (detected %s environment)", ciName)
83 } else {
84 msg += " (stdin is not a TTY)"
85 }
86
87 if len(alternatives) > 0 {
88 msg += "\n\nAlternatives:"
89 for _, alt := range alternatives {
90 msg += fmt.Sprintf("\n - %s", alt)
91 }
92 }
93
94 return errors.New(msg)
95}
96
97// ErrorInteractiveFlag returns a formatted error message indicating that an
98// interactive flag cannot be used in a non-TTY environment.
99func ErrorInteractiveFlag(flagName string) error {
100 msg := fmt.Sprintf("flag '%s' requires an interactive terminal", flagName)
101
102 if IsCI() {
103 ciName := GetCIName()
104 msg += fmt.Sprintf(" (detected %s environment)", ciName)
105 } else {
106 msg += " (stdin is not a TTY)"
107 }
108
109 return errors.New(msg)
110}