diff options
author | Kalyan Sriram <kalyan@coderkalyan.com> | 2021-11-22 18:37:40 +0000 |
---|---|---|
committer | Hubert Hirtz <hubert@hirtz.pm> | 2021-11-23 09:10:59 +0100 |
commit | b46900566104c0143e06df452fb994e888b23548 (patch) | |
tree | 2d4f0e14c069fd8a4a7ff91ea9ee43d87ad66b58 | |
parent | Add support for CHATHISTORY TARGETS (diff) |
config: replace YAML with scfg config format
This patch replaces the YAML configuration format with scfg
(https://git.sr.ht/~emersion/scfg).
Additionally, a few things about configuration are cleaned up:
* abbreviated names are expanded (addr -> address, nick -> nickname)
* negative bools switched to positive (no-tls -> tls)
* independent column widths are grouped under the "pane-width"
directive
* implementation of default configuration values is improved
* password-cmd is executed directly (with scfg field parsing)
instead of with "sh -c".
* on-highlight is now a file, $XDG_CONFIG_HOME/senpai/highlight by
default, which can be changed with the on-highlight-path directive
-rw-r--r-- | README.md | 10 | ||||
-rw-r--r-- | app.go | 48 | ||||
-rw-r--r-- | cmd/senpai/main.go | 2 | ||||
-rw-r--r-- | cmd/test/main.go | 4 | ||||
-rw-r--r-- | config.go | 272 | ||||
-rw-r--r-- | doc/senpai.1.scd | 2 | ||||
-rw-r--r-- | doc/senpai.5.scd | 157 | ||||
-rw-r--r-- | go.mod | 2 | ||||
-rw-r--r-- | go.sum | 12 |
9 files changed, 358 insertions, 151 deletions
@@ -14,12 +14,12 @@ senpai is an IRC client that works best with bouncers: ```shell mkdir -p ~/.config/senpai -cat <<EOF >~/.config/senpai/senpai.yaml -addr: chat.sr.ht -nick: senpai -password: "my password can't be this cute (2010)" +cat <<EOF >~/.config/senpai/senpai.scfg +address chat.sr.ht +nickname senpai +password "my password can't be this cute (2010)" # alternatively, specify a command to fetch your password: -# password-cmd: "gopass show irc/<username>" +# password-cmd gopass show irc/<username> EOF go run ./cmd/senpai ``` @@ -2,6 +2,7 @@ package senpai import ( "crypto/tls" + "errors" "fmt" "net" "os" @@ -111,10 +112,7 @@ func NewApp(cfg Config) (app *App, err error) { } } - mouse := true - if cfg.Mouse != nil { - mouse = *cfg.Mouse - } + mouse := cfg.Mouse app.win, err = ui.New(ui.Config{ NickColWidth: cfg.NickColWidth, @@ -320,10 +318,10 @@ func (app *App) tryConnect() (conn net.Conn, err error) { if colonIdx <= bracketIdx { // either colonIdx < 0, or the last colon is before a ']' (end // of IPv6 address. -> missing port - if app.cfg.NoTLS { - addr += ":6667" - } else { + if app.cfg.TLS { addr += ":6697" + } else { + addr += ":6667" } } @@ -332,7 +330,7 @@ func (app *App) tryConnect() (conn net.Conn, err error) { return } - if !app.cfg.NoTLS { + if app.cfg.TLS { host, _, _ := net.SplitHostPort(addr) // should succeed since net.Dial did. conn = tls.Client(conn, &tls.Config{ ServerName: host, @@ -889,22 +887,38 @@ func (app *App) isHighlight(s *irc.Session, content string) bool { return false } -// notifyHighlight executes the "on-highlight" command according to the given +// notifyHighlight executes the script at "on-highlight-path" according to the given // message context. func (app *App) notifyHighlight(buffer, nick, content string) { - if app.cfg.OnHighlight == "" { - return + path := app.cfg.OnHighlightPath + if path == "" { + defaultHighlightPath, err := DefaultHighlightPath() + if err != nil { + return + } + path = defaultHighlightPath } - sh, err := exec.LookPath("sh") - if err != nil { + + netID, curBuffer := app.win.CurrentBuffer() + if _, err := os.Stat(app.cfg.OnHighlightPath); errors.Is(err, os.ErrNotExist) { + // only error out if the user specified a highlight path + // if default path unreachable, simple bail + if app.cfg.OnHighlightPath != "" { + body := fmt.Sprintf("Unable to find on-highlight command at path: %q", app.cfg.OnHighlightPath) + app.addStatusLine(netID, ui.Line{ + At: time.Now(), + Head: "!!", + HeadColor: tcell.ColorRed, + Body: ui.PlainString(body), + }) + } return } - netID, curBuffer := app.win.CurrentBuffer() here := "0" if buffer == curBuffer { // TODO also check netID here = "1" } - cmd := exec.Command(sh, "-c", app.cfg.OnHighlight) + cmd := exec.Command(app.cfg.OnHighlightPath) cmd.Env = append(os.Environ(), fmt.Sprintf("BUFFER=%s", buffer), fmt.Sprintf("HERE=%s", here), @@ -913,7 +927,7 @@ func (app *App) notifyHighlight(buffer, nick, content string) { ) output, err := cmd.CombinedOutput() if err != nil { - body := fmt.Sprintf("Failed to invoke on-highlight command: %v. Output: %q", err, string(output)) + body := fmt.Sprintf("Failed to invoke on-highlight command at path: %v. Output: %q", err, string(output)) app.addStatusLine(netID, ui.Line{ At: time.Now(), Head: "!!", @@ -928,7 +942,7 @@ func (app *App) notifyHighlight(buffer, nick, content string) { func (app *App) typing() { netID, buffer := app.win.CurrentBuffer() s := app.sessions[netID] - if s == nil || app.cfg.NoTypings { + if s == nil || !app.cfg.Typings { return } if buffer == "" { diff --git a/cmd/senpai/main.go b/cmd/senpai/main.go index 675228f..665cacc 100644 --- a/cmd/senpai/main.go +++ b/cmd/senpai/main.go @@ -34,7 +34,7 @@ func main() { if err != nil { panic(err) } - configPath = path.Join(configDir, "senpai", "senpai.yaml") + configPath = path.Join(configDir, "senpai", "senpai.scfg") } cfg, err := senpai.LoadConfigFile(configPath) diff --git a/cmd/test/main.go b/cmd/test/main.go index 804937f..6eb0574 100644 --- a/cmd/test/main.go +++ b/cmd/test/main.go @@ -108,7 +108,7 @@ func parseFlags() { if err != nil { panic(err) } - configPath = path.Join(configDir, "senpai", "senpai.yaml") + configPath = path.Join(configDir, "senpai", "senpai.scfg") } cfg, err := senpai.LoadConfigFile(configPath) @@ -121,6 +121,6 @@ func parseFlags() { if cfg.Password != nil { password = *cfg.Password } - useTLS = !cfg.NoTLS + useTLS = cfg.TLS } } @@ -3,21 +3,20 @@ package senpai import ( "errors" "fmt" - "io/ioutil" + "os" "os/exec" + "path" "strconv" "strings" "github.com/gdamore/tcell/v2" - "gopkg.in/yaml.v2" + "git.sr.ht/~emersion/go-scfg" ) type Color tcell.Color -func (c *Color) UnmarshalText(data []byte) error { - s := string(data) - +func parseColor(s string, c *Color) error { if strings.HasPrefix(s, "#") { hex, err := strconv.ParseInt(s[1:], 16, 32) if err != nil { @@ -47,34 +46,73 @@ func (c *Color) UnmarshalText(data []byte) error { return nil } +type ConfigColors struct { + Prompt Color +} + type Config struct { - Addr string - Nick string - Real string - User string - Password *string - PasswordCmd string `yaml:"password-cmd"` - NoTLS bool `yaml:"no-tls"` - Channels []string - - NoTypings bool `yaml:"no-typings"` - Mouse *bool - - Highlights []string - OnHighlight string `yaml:"on-highlight"` - NickColWidth int `yaml:"nick-column-width"` - ChanColWidth int `yaml:"chan-column-width"` - MemberColWidth int `yaml:"member-column-width"` - - Colors struct { - Prompt Color - } + Addr string + Nick string + Real string + User string + Password *string + TLS bool + Channels []string + + Typings bool + Mouse bool + + Highlights []string + OnHighlightPath string + NickColWidth int + ChanColWidth int + MemberColWidth int + + Colors ConfigColors Debug bool } -func ParseConfig(buf []byte) (cfg Config, err error) { - err = yaml.Unmarshal(buf, &cfg) +func DefaultHighlightPath() (string, error) { + configDir, err := os.UserConfigDir() + if err != nil { + return "", err + } + return path.Join(configDir, "senpai", "highlight"), nil +} + +func Defaults() (cfg Config, err error) { + cfg = Config{ + Addr: "", + Nick: "", + Real: "", + User: "", + Password: nil, + TLS: true, + Channels: nil, + Typings: true, + Mouse: true, + Highlights: nil, + OnHighlightPath: "", + NickColWidth: 16, + ChanColWidth: 0, + MemberColWidth: 0, + Colors: ConfigColors{ + Prompt: Color(tcell.ColorDefault), + }, + Debug: false, + } + + return +} + +func ParseConfig(filename string) (cfg Config, err error) { + cfg, err = Defaults() + if err != nil { + return + } + + err = unmarshal(filename, &cfg) if err != nil { return cfg, err } @@ -90,45 +128,165 @@ func ParseConfig(buf []byte) (cfg Config, err error) { if cfg.Real == "" { cfg.Real = cfg.Nick } - if cfg.PasswordCmd != "" { - password, err := runPasswordCmd(cfg.PasswordCmd) - if err != nil { - return cfg, err - } - cfg.Password = &password - } - if cfg.NickColWidth <= 0 { - cfg.NickColWidth = 16 - } - if cfg.ChanColWidth < 0 { - cfg.ChanColWidth = 0 - } - if cfg.MemberColWidth < 0 { - cfg.MemberColWidth = 0 - } return } func LoadConfigFile(filename string) (cfg Config, err error) { - var buf []byte - - buf, err = ioutil.ReadFile(filename) - if err != nil { - return cfg, fmt.Errorf("failed to read the file: %s", err) - } - - cfg, err = ParseConfig(buf) + cfg, err = ParseConfig(filename) if err != nil { return cfg, fmt.Errorf("invalid content found in the file: %s", err) } return } -func runPasswordCmd(command string) (password string, err error) { - cmd := exec.Command("sh", "-c", command) - stdout, err := cmd.Output() - if err == nil { - password = strings.TrimSuffix(string(stdout), "\n") +func unmarshal(filename string, cfg *Config) (err error) { + directives, err := scfg.Load(filename) + if err != nil { + return fmt.Errorf("error parsing scfg: %s", err) + } + + for _, d := range directives { + switch d.Name { + case "address": + if err := d.ParseParams(&cfg.Addr); err != nil { + return err + } + case "nickname": + if err := d.ParseParams(&cfg.Nick); err != nil { + return err + } + case "username": + if err := d.ParseParams(&cfg.User); err != nil { + return err + } + case "realname": + if err := d.ParseParams(&cfg.Real); err != nil { + return err + } + case "password": + // if a password-cmd is provided, don't use this value + if directives.Get("password-cmd") != nil { + continue + } + + var password string + if err := d.ParseParams(&password); err != nil { + return err + } + cfg.Password = &password + case "password-cmd": + var cmdName string + if err := d.ParseParams(&cmdName); err != nil { + return err + } + + cmd := exec.Command(cmdName, d.Params[1:]...) + var stdout []byte + if stdout, err = cmd.Output(); err != nil { + return fmt.Errorf("error running password command: %s", err) + } + + password := strings.TrimSuffix(string(stdout), "\n") + cfg.Password = &password + case "channel": + // TODO: does this work with soju.im/bouncer-networks extension? + cfg.Channels = append(cfg.Channels, d.Params...) + case "highlight": + cfg.Highlights = append(cfg.Highlights, d.Params...) + case "on-highlight-path": + if err := d.ParseParams(&cfg.OnHighlightPath); err != nil { + return err + } + case "pane-widths": + for _, child := range d.Children { + switch child.Name { + case "nicknames": + var nicknames string + if err := child.ParseParams(&nicknames); err != nil { + return err + } + + if cfg.NickColWidth, err = strconv.Atoi(nicknames); err != nil { + return err + } + case "channels": + var channels string + if err := child.ParseParams(&channels); err != nil { + return err + } + + if cfg.ChanColWidth, err = strconv.Atoi(channels); err != nil { + return err + } + case "members": + var members string + if err := child.ParseParams(&members); err != nil { + return err + } + + if cfg.MemberColWidth, err = strconv.Atoi(members); err != nil { + return err + } + default: + return fmt.Errorf("unknown directive %q", child.Name) + } + } + case "tls": + var tls string + if err := d.ParseParams(&tls); err != nil { + return err + } + + if cfg.TLS, err = strconv.ParseBool(tls); err != nil { + return err + } + case "typings": + var typings string + if err := d.ParseParams(&typings); err != nil { + return err + } + + if cfg.Typings, err = strconv.ParseBool(typings); err != nil { + return err + } + case "mouse": + var mouse string + if err := d.ParseParams(&mouse); err != nil { + return err + } + + if cfg.Mouse, err = strconv.ParseBool(mouse); err != nil { + return err + } + case "colors": + for _, child := range d.Children { + switch child.Name { + case "prompt": + var prompt string + if err := child.ParseParams(&prompt); err != nil { + return err + } + + fmt.Println(prompt) + if err = parseColor(prompt, &cfg.Colors.Prompt); err != nil { + return err + } + default: + return fmt.Errorf("unknown directive %q", child.Name) + } + } + case "debug": + var debug string + if err := d.ParseParams(&debug); err != nil { + return err + } + + if cfg.Debug, err = strconv.ParseBool(debug); err != nil { + return err + } + default: + return fmt.Errorf("unknown directive %q", d.Name) + } } return diff --git a/doc/senpai.1.scd b/doc/senpai.1.scd index df943aa..7517d14 100644 --- a/doc/senpai.1.scd +++ b/doc/senpai.1.scd @@ -31,7 +31,7 @@ extensions, such as: senpai needs a configuration file to start. It searches for it in the following location: - $XDG_CONFIG_HOME/senpai/senpai.yaml + $XDG_CONFIG_HOME/senpai/senpai.scfg If unset, $XDG_CONFIG_HOME defaults to *~/.config*. diff --git a/doc/senpai.5.scd b/doc/senpai.5.scd index 4121297..1d49e51 100644 --- a/doc/senpai.5.scd +++ b/doc/senpai.5.scd @@ -6,34 +6,35 @@ senpai - Configuration file format and settings # DESCRIPTION -A senpai configuration file is a YAML file. +A senpai configuration file is a scfg file (see https://git.sr.ht/~emersion/scfg). +The config file has one directive per line. Some settings are required, the others are optional. # SETTINGS -*addr* (required) +*address* (required) The address (_host[:port]_) of the IRC server. senpai uses TLS connections by default unless you specify *no-tls* option. TLS connections default to port 6697, plain-text use port 6667. -*nick* (required) +*nickname* (required) Your nickname, sent with a _NICK_ IRC message. It mustn't contain spaces or colons (*:*). -*real* +*realname* Your real name, or actually just a field that will be available to others and may contain spaces and colons. Sent with the _USER_ IRC message. By default, the value of *nick* is used. -*user* +*username* Your username, sent with the _USER_ IRC message and also used for SASL authentication. By default, the value of *nick* is used. *password* Your password, used for SASL authentication. See also *password-cmd*. -*password-cmd* +*password-cmd* command [arguments...] Alternatively to providing your SASL authentication password directly in plaintext, you can specify a command to be run to fetch the password at runtime. This is useful if you store your passwords in a separate (probably @@ -41,18 +42,31 @@ Some settings are required, the others are optional. _pass_ or _gopass_. If a *password-cmd* is provided, the value of *password* will be ignored and the output of *password-cmd* will be used for login. -*channels* - A list of channel names that senpai will automatically join at startup and - server reconnect. +*channel* + A spaced separated list of channel names that senpai will automatically join + at startup and server reconnect. This directive can be specified multiple times. -*highlights* - A list of keywords that will trigger a notification and a display indicator - when said by others. By default, senpai will use your current nickname. +*highlight* + A space separated list of keywords that will trigger a notification and a + display indicator when said by others. This directive can be specified + multiple times. -*on-highlight* - A command to be executed via _sh_ when you are highlighted. The following - environment variables are set with repect to the highlight, THEY MUST APPEAR - QUOTED IN THE SETTING, OR YOU WILL BE OPEN TO SHELL INJECTION ATTACKS. + By default, senpai will use your current nickname. + +*on-highlight-path* + Alternative path to a shell script to be executed when you are highlighted. By default, + senpai looks for a highlight shell script at $XDG_CONFIG_HOME/senpai/highlight. + If no file is found at that path, and an alternate path is not provided, + highlight command execution is disabled. + + If unset, $XDG_CONFIG_HOME defaults to *~/.config/*. + + Before the highlight script is executed, the following environment + variables are populated: + + Shell scripts MUST ENSURE VARIABLES appear QUOTED in the script file, + OR YOU WILL BE OPEN TO SHELL INJECTION ATTACKS. Shell scripts must also + ensure characters like '\*' and '?' are not expanded. [[ *Environment variable* :< *Description* @@ -72,65 +86,80 @@ Some settings are required, the others are optional. To get around this, you can double the backslash with the following snippet: ``` -on-highlight: | - escape() { - printf "%s" "$1" | sed 's#\\#\\\\#g' - } - notify-send "[$BUFFER] $SENDER" "$(escape "$MESSAGE")" +#!/bin/sh +escape() { + printf "%s" "$1" | sed 's#\\#\\\\#g' +} + +notify-send "[$BUFFER] $SENDER" "$(escape "$MESSAGE")" ``` -*nick-column-width* - The number of cells that the column for nicknames occupies in the timeline. - By default, 16. +*pane-widths* { ... } + Configure the width of various UI panes. -*chan-column-width* - Make the channel list vertical, with a width equals to the given amount of - cells. By default, the channel list is horizontal. + Pane widths are set as sub-directives of the main *pane-widths* directive: + +``` +pane-widths { + nicknames 16 +} +``` -*member-column-width* - Show the list of channel members on the right of the screen, with a width - equals to the given amount of cells. + This directive supports the following sub-directives: -*no-tls* - Disable TLS encryption. Defaults to false. + *nicknames* + The number of cells that the column for nicknames occupies in the timeline. + By default, 16. -*no-typings* - Prevent senpai from sending typing notifications which let others know when - you are typing a message. Defaults to false. + *channels* + Make the channel list vertical, with a width equals to the given amount of + cells. By default, the channel list is horizontal. + + *members* + Show the list of channel members on the right of the screen, with a width + equals to the given amount of cells. + +*tls* + Enable TLS encryption. Defaults to true. + +*typings* + Send typing notifications which let others know when you are typing a message. + Defaults to true. *mouse* Enable or disable mouse support. Defaults to true. -*colors* +*colors* { ... } Settings for colors of different UI elements. Colors are represented as numbers from 0 to 255 for 256 default terminal colors respectively. -1 has special meaning of default terminal color. To use true colors, *#*_rrggbb_ notation is supported. - Colors are set as sub-options of the main *colors* option: + Colors are set as sub-directives of the main *colors* directive: ``` -colors: - prompt: 3 # green +colors { + prompt 3 # green +} ``` -[[ *Sub-option* +[[ *Sub-directive* :< *Description* | prompt : color for ">"-prompt that appears in command mode *debug* Dump all sent and received data to the home buffer, useful for debugging. - By default, false. + Defaults to false. # EXAMPLES A minimal configuration file to connect to Libera.Chat as "Guest123456": ``` -addr: irc.libera.chat -nick: Guest123456 +address irc.libera.chat +nickname Guest123456 ``` A more advanced configuration file that enables SASL authentication, fetches the @@ -140,24 +169,30 @@ notifications on highlight and decreases the width of the nick column to 12 need to know if the terminal emulator that runs senpai has focus): ``` -addr: irc.libera.chat -nick: Guest123456 -user: senpai -real: Guest von Lenon -password-cmd: "gopass show irc/guest" # use your favorite CLI password solution here -channels: ["#rahxephon"] -highlights: - - guest - - senpai -on-highlight: | - escape() { - printf "%s" "$1" | sed 's#\\#\\\\#g' - } - FOCUS=$(swaymsg -t get_tree | jq '..|objects|select(.focused==true)|.name' | grep senpai | wc -l) - if [ "$HERE" -eq 0 ] || [ $FOCUS -eq 0 ]; then - notify-send "[$BUFFER] $SENDER" "$(escape "$MESSAGE")" - fi -nick-column-width: 12 +address irc.libera.chat +nickname Guest123456 +username senpai +realname "Guest von Lenon" +password-cmd gopass show irc/guest # use your favorite CLI password solution here +channel "#rahxephon" +highlight guest senpai +highlight lenon # don't know why you'd split it into multiple lines, but you can if you want +pane-widths { + nicknames 12 +} +``` + +And the highlight file (*~/.config/senpai/highlight*): +``` +#!/bin/sh + +escape() { + printf "%s" "$1" | sed 's#\\#\\\\#g' +} +FOCUS=$(swaymsg -t get_tree | jq '..|objects|select(.focused==true)|.name' | grep senpai | wc -l) +if [ "$HERE" -eq 0 ] || [ $FOCUS -eq 0 ]; then + notify-send "[$BUFFER] $SENDER" "$(escape "$MESSAGE")" +fi ``` # SEE ALSO @@ -3,11 +3,11 @@ module git.sr.ht/~taiite/senpai go 1.16 require ( + git.sr.ht/~emersion/go-scfg v0.0.0-20201019143924-142a8aa629fc github.com/gdamore/tcell/v2 v2.3.11 github.com/mattn/go-runewidth v0.0.10 golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 - gopkg.in/yaml.v2 v2.3.0 mvdan.cc/xurls/v2 v2.3.0 ) @@ -1,11 +1,15 @@ +git.sr.ht/~emersion/go-scfg v0.0.0-20201019143924-142a8aa629fc h1:51BD67xFX+bozd3ZRuOUfalrhx4/nQSh6A9lI08rYOk= +git.sr.ht/~emersion/go-scfg v0.0.0-20201019143924-142a8aa629fc/go.mod h1:t+Ww6SR24yYnXzEWiNlOY0AFo5E9B73X++10lrSpp4U= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/hhirtz/tcell/v2 v2.3.12-0.20210807133752-5d743c3ab0c9 h1:YE0ZsDHfDGR0MeB6YLSGW8tjoxOXZKX3XbB0ytGDX4M= github.com/hhirtz/tcell/v2 v2.3.12-0.20210807133752-5d743c3ab0c9/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= @@ -23,11 +27,7 @@ golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 h1:Vv0JUPWTyeqUq42B2WJ1FeIDjjvGKoA2Ss+Ts0lAVbs= golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= mvdan.cc/xurls/v2 v2.3.0 h1:59Olnbt67UKpxF1EwVBopJvkSUBmgtb468E4GVWIZ1I= mvdan.cc/xurls/v2 v2.3.0/go.mod h1:AjuTy7gEiUArFMjgBBDU4SMxlfUYsRokpJQgNWOt3e4= |