M README.md => README.md +5 -5
@@ 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
```
M app.go => app.go +31 -17
@@ 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 == "" {
M cmd/senpai/main.go => cmd/senpai/main.go +1 -1
@@ 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)
M cmd/test/main.go => cmd/test/main.go +2 -2
@@ 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
}
}
M config.go => config.go +215 -57
@@ 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
M doc/senpai.1.scd => doc/senpai.1.scd +1 -1
@@ 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*.
M doc/senpai.5.scd => doc/senpai.5.scd +96 -61
@@ 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
M go.mod => go.mod +1 -1
@@ 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
)
M go.sum => go.sum +6 -6
@@ 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=