...

Source file src/github.com/cli/go-gh/v2/pkg/term/env.go

Documentation: github.com/cli/go-gh/v2/pkg/term

     1  // Package term provides information about the terminal that the current process is connected to (if any),
     2  // for example measuring the dimensions of the terminal and inspecting whether it's safe to output color.
     3  package term
     4  
     5  import (
     6  	"io"
     7  	"os"
     8  	"strconv"
     9  	"strings"
    10  
    11  	"github.com/muesli/termenv"
    12  	"golang.org/x/term"
    13  )
    14  
    15  // Term represents information about the terminal that a process is connected to.
    16  type Term struct {
    17  	in           *os.File
    18  	out          *os.File
    19  	errOut       *os.File
    20  	isTTY        bool
    21  	colorEnabled bool
    22  	is256enabled bool
    23  	hasTrueColor bool
    24  	width        int
    25  	widthPercent int
    26  }
    27  
    28  // FromEnv initializes a Term from [os.Stdout] and environment variables:
    29  //   - GH_FORCE_TTY
    30  //   - NO_COLOR
    31  //   - CLICOLOR
    32  //   - CLICOLOR_FORCE
    33  //   - TERM
    34  //   - COLORTERM
    35  func FromEnv() Term {
    36  	var stdoutIsTTY bool
    37  	var isColorEnabled bool
    38  	var termWidthOverride int
    39  	var termWidthPercentage int
    40  
    41  	spec := os.Getenv("GH_FORCE_TTY")
    42  	if spec != "" {
    43  		stdoutIsTTY = true
    44  		isColorEnabled = !IsColorDisabled()
    45  
    46  		if w, err := strconv.Atoi(spec); err == nil {
    47  			termWidthOverride = w
    48  		} else if strings.HasSuffix(spec, "%") {
    49  			if p, err := strconv.Atoi(spec[:len(spec)-1]); err == nil {
    50  				termWidthPercentage = p
    51  			}
    52  		}
    53  	} else {
    54  		stdoutIsTTY = IsTerminal(os.Stdout)
    55  		isColorEnabled = IsColorForced() || (!IsColorDisabled() && stdoutIsTTY)
    56  	}
    57  
    58  	isVirtualTerminal := false
    59  	if stdoutIsTTY {
    60  		if err := enableVirtualTerminalProcessing(os.Stdout); err == nil {
    61  			isVirtualTerminal = true
    62  		}
    63  	}
    64  
    65  	return Term{
    66  		in:           os.Stdin,
    67  		out:          os.Stdout,
    68  		errOut:       os.Stderr,
    69  		isTTY:        stdoutIsTTY,
    70  		colorEnabled: isColorEnabled,
    71  		is256enabled: isVirtualTerminal || is256ColorSupported(),
    72  		hasTrueColor: isVirtualTerminal || isTrueColorSupported(),
    73  		width:        termWidthOverride,
    74  		widthPercent: termWidthPercentage,
    75  	}
    76  }
    77  
    78  // In is the reader reading from standard input.
    79  func (t Term) In() io.Reader {
    80  	return t.in
    81  }
    82  
    83  // Out is the writer writing to standard output.
    84  func (t Term) Out() io.Writer {
    85  	return t.out
    86  }
    87  
    88  // ErrOut is the writer writing to standard error.
    89  func (t Term) ErrOut() io.Writer {
    90  	return t.errOut
    91  }
    92  
    93  // IsTerminalOutput returns true if standard output is connected to a terminal.
    94  func (t Term) IsTerminalOutput() bool {
    95  	return t.isTTY
    96  }
    97  
    98  // IsColorEnabled reports whether it's safe to output ANSI color sequences, depending on IsTerminalOutput
    99  // and environment variables.
   100  func (t Term) IsColorEnabled() bool {
   101  	return t.colorEnabled
   102  }
   103  
   104  // Is256ColorSupported reports whether the terminal advertises ANSI 256 color codes.
   105  func (t Term) Is256ColorSupported() bool {
   106  	return t.is256enabled
   107  }
   108  
   109  // IsTrueColorSupported reports whether the terminal advertises support for ANSI true color sequences.
   110  func (t Term) IsTrueColorSupported() bool {
   111  	return t.hasTrueColor
   112  }
   113  
   114  // Size returns the width and height of the terminal that the current process is attached to.
   115  // In case of errors, the numeric values returned are -1.
   116  func (t Term) Size() (int, int, error) {
   117  	if t.width > 0 {
   118  		return t.width, -1, nil
   119  	}
   120  
   121  	ttyOut := t.out
   122  	if ttyOut == nil || !IsTerminal(ttyOut) {
   123  		if f, err := openTTY(); err == nil {
   124  			defer f.Close()
   125  			ttyOut = f
   126  		} else {
   127  			return -1, -1, err
   128  		}
   129  	}
   130  
   131  	width, height, err := terminalSize(ttyOut)
   132  	if err == nil && t.widthPercent > 0 {
   133  		return int(float64(width) * float64(t.widthPercent) / 100), height, nil
   134  	}
   135  
   136  	return width, height, err
   137  }
   138  
   139  // Theme returns the theme of the terminal by analyzing the background color of the terminal.
   140  func (t Term) Theme() string {
   141  	if !t.IsColorEnabled() {
   142  		return "none"
   143  	}
   144  	if termenv.HasDarkBackground() {
   145  		return "dark"
   146  	}
   147  	return "light"
   148  }
   149  
   150  // IsTerminal reports whether a file descriptor is connected to a terminal.
   151  func IsTerminal(f *os.File) bool {
   152  	return term.IsTerminal(int(f.Fd()))
   153  }
   154  
   155  func terminalSize(f *os.File) (int, int, error) {
   156  	return term.GetSize(int(f.Fd()))
   157  }
   158  
   159  // IsColorDisabled returns true if environment variables NO_COLOR or CLICOLOR prohibit usage of color codes
   160  // in terminal output.
   161  func IsColorDisabled() bool {
   162  	return os.Getenv("NO_COLOR") != "" || os.Getenv("CLICOLOR") == "0"
   163  }
   164  
   165  // IsColorForced returns true if environment variable CLICOLOR_FORCE is set to force colored terminal output.
   166  func IsColorForced() bool {
   167  	return os.Getenv("CLICOLOR_FORCE") != "" && os.Getenv("CLICOLOR_FORCE") != "0"
   168  }
   169  
   170  func is256ColorSupported() bool {
   171  	return isTrueColorSupported() ||
   172  		strings.Contains(os.Getenv("TERM"), "256") ||
   173  		strings.Contains(os.Getenv("COLORTERM"), "256")
   174  }
   175  
   176  func isTrueColorSupported() bool {
   177  	term := os.Getenv("TERM")
   178  	colorterm := os.Getenv("COLORTERM")
   179  
   180  	return strings.Contains(term, "24bit") ||
   181  		strings.Contains(term, "truecolor") ||
   182  		strings.Contains(colorterm, "24bit") ||
   183  		strings.Contains(colorterm, "truecolor")
   184  }
   185  

View as plain text