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 }