github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/cmd/snap/color.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2018 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package main
    21  
    22  import (
    23  	"fmt"
    24  	"os"
    25  	"strings"
    26  
    27  	"golang.org/x/crypto/ssh/terminal"
    28  
    29  	"github.com/snapcore/snapd/i18n"
    30  	"github.com/snapcore/snapd/snap"
    31  )
    32  
    33  type unicodeMixin struct {
    34  	Unicode string `long:"unicode" default:"auto" choice:"auto" choice:"never" choice:"always"`
    35  }
    36  
    37  func (ux unicodeMixin) addUnicodeChars(esc *escapes) {
    38  	if canUnicode(ux.Unicode) {
    39  		esc.dash = "–" // that's an en dash (so yaml is happy)
    40  		esc.uparrow = "↑"
    41  		esc.tick = "✓"
    42  	} else {
    43  		esc.dash = "--" // two dashes keeps yaml happy also
    44  		esc.uparrow = "^"
    45  		esc.tick = "*"
    46  	}
    47  }
    48  
    49  func (ux unicodeMixin) getEscapes() *escapes {
    50  	esc := &escapes{}
    51  	ux.addUnicodeChars(esc)
    52  	return esc
    53  }
    54  
    55  type colorMixin struct {
    56  	Color string `long:"color" default:"auto" choice:"auto" choice:"never" choice:"always"`
    57  	unicodeMixin
    58  }
    59  
    60  func (mx colorMixin) getEscapes() *escapes {
    61  	esc := colorTable(mx.Color)
    62  	mx.addUnicodeChars(&esc)
    63  	return &esc
    64  }
    65  
    66  func canUnicode(mode string) bool {
    67  	switch mode {
    68  	case "always":
    69  		return true
    70  	case "never":
    71  		return false
    72  	}
    73  	if !isStdoutTTY {
    74  		return false
    75  	}
    76  	var lang string
    77  	for _, k := range []string{"LC_MESSAGES", "LC_ALL", "LANG"} {
    78  		lang = os.Getenv(k)
    79  		if lang != "" {
    80  			break
    81  		}
    82  	}
    83  	if lang == "" {
    84  		return false
    85  	}
    86  	lang = strings.ToUpper(lang)
    87  	return strings.Contains(lang, "UTF-8") || strings.Contains(lang, "UTF8")
    88  }
    89  
    90  var isStdoutTTY = terminal.IsTerminal(1)
    91  
    92  func colorTable(mode string) escapes {
    93  	switch mode {
    94  	case "always":
    95  		return color
    96  	case "never":
    97  		return noesc
    98  	}
    99  	if !isStdoutTTY {
   100  		return noesc
   101  	}
   102  	if _, ok := os.LookupEnv("NO_COLOR"); ok {
   103  		// from http://no-color.org/:
   104  		//   command-line software which outputs text with ANSI color added should
   105  		//   check for the presence of a NO_COLOR environment variable that, when
   106  		//   present (regardless of its value), prevents the addition of ANSI color.
   107  		return mono // bold & dim is still ok
   108  	}
   109  	if term := os.Getenv("TERM"); term == "xterm-mono" || term == "linux-m" {
   110  		// these are often used to flag "I don't want to see color" more than "I can't do color"
   111  		// (if you can't *do* color, `color` and `mono` should produce the same results)
   112  		return mono
   113  	}
   114  	return color
   115  }
   116  
   117  var colorDescs = mixinDescs{
   118  	// TRANSLATORS: This should not start with a lowercase letter.
   119  	"color":   i18n.G("Use a little bit of color to highlight some things."),
   120  	"unicode": unicodeDescs["unicode"],
   121  }
   122  
   123  var unicodeDescs = mixinDescs{
   124  	// TRANSLATORS: This should not start with a lowercase letter.
   125  	"unicode": i18n.G("Use a little bit of Unicode to improve legibility."),
   126  }
   127  
   128  type escapes struct {
   129  	green string
   130  	bold  string
   131  	end   string
   132  
   133  	tick, dash, uparrow string
   134  }
   135  
   136  var (
   137  	color = escapes{
   138  		green: "\033[32m",
   139  		bold:  "\033[1m",
   140  		end:   "\033[0m",
   141  	}
   142  
   143  	mono = escapes{
   144  		green: "\033[1m",
   145  		bold:  "\033[1m",
   146  		end:   "\033[0m",
   147  	}
   148  
   149  	noesc = escapes{}
   150  )
   151  
   152  // fillerPublisher is used to add an no-op escape sequence to a header in a
   153  // tabwriter table, so that things line up.
   154  func fillerPublisher(esc *escapes) string {
   155  	return esc.green + esc.end
   156  }
   157  
   158  // longPublisher returns a string that'll present the publisher of a snap to the
   159  // terminal user:
   160  //
   161  // * if the publisher's username and display name match, it's just the display
   162  //   name; otherwise, it'll include the username in parentheses
   163  //
   164  // * if the publisher is verified, it'll include a green check mark; otherwise,
   165  //   it'll include a no-op escape sequence of the same length as the escape
   166  //   sequence used to make it green (this so that tabwriter gets things right).
   167  func longPublisher(esc *escapes, storeAccount *snap.StoreAccount) string {
   168  	if storeAccount == nil {
   169  		return esc.dash + esc.green + esc.end
   170  	}
   171  	badge := ""
   172  	if storeAccount.Validation == "verified" {
   173  		badge = esc.tick
   174  	}
   175  	// NOTE this makes e.g. 'Potato' == 'potato', and 'Potato Team' == 'potato-team',
   176  	// but 'Potato Team' != 'potatoteam', 'Potato Inc.' != 'potato' (in fact 'Potato Inc.' != 'potato-inc')
   177  	if strings.EqualFold(strings.Replace(storeAccount.Username, "-", " ", -1), storeAccount.DisplayName) {
   178  		return storeAccount.DisplayName + esc.green + badge + esc.end
   179  	}
   180  	return fmt.Sprintf("%s (%s%s%s%s)", storeAccount.DisplayName, storeAccount.Username, esc.green, badge, esc.end)
   181  }
   182  
   183  // shortPublisher returns a string that'll present the publisher of a snap to the
   184  // terminal user:
   185  //
   186  // * it'll always be just the username
   187  //
   188  // * if the publisher is verified, it'll include a green check mark; otherwise,
   189  //   it'll include a no-op escape sequence of the same length as the escape
   190  //   sequence used to make it green (this so that tabwriter gets things right).
   191  func shortPublisher(esc *escapes, storeAccount *snap.StoreAccount) string {
   192  	if storeAccount == nil {
   193  		return "-" + esc.green + esc.end
   194  	}
   195  	badge := ""
   196  	if storeAccount.Validation == "verified" {
   197  		badge = esc.tick
   198  	}
   199  	return storeAccount.Username + esc.green + badge + esc.end
   200  
   201  }