~taiite/public-inbox

1

[PATCH senpai v4 2/2] config: replace YAML with scfg config format

Details
Message ID
<0101017d48f07fb7-ae157ba3-588c-42d4-a9ee-c0d6f70080d2-000000@us-west-2.amazonses.com>
DKIM signature
pass
Download raw message
Patch: +358 -151
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
---
 README.md          |  10 +-
 app.go             |  48 +++++---
 cmd/senpai/main.go |   2 +-
 cmd/test/main.go   |   4 +-
 config.go          | 272 +++++++++++++++++++++++++++++++++++----------
 doc/senpai.1.scd   |   2 +-
 doc/senpai.5.scd   | 157 ++++++++++++++++----------
 go.mod             |   2 +-
 go.sum             |  12 +-
 9 files changed, 358 insertions(+), 151 deletions(-)

diff --git a/README.md b/README.md
index 8ad8467..7dec286 100644
--- a/README.md
+++ b/README.md
@@ -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
```
diff --git a/app.go b/app.go
index d652b97..6ea9f44 100644
--- a/app.go
+++ b/app.go
@@ -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
	}
}
diff --git a/config.go b/config.go
index be6dcbc..9f216fa 100644
--- a/config.go
+++ b/config.go
@@ -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("error loading config 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
diff --git a/go.mod b/go.mod
index 88829bd..d3c9b55 100644
--- a/go.mod
+++ b/go.mod
@@ -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
)

diff --git a/go.sum b/go.sum
index 634a5bd..856f865 100644
--- a/go.sum
+++ b/go.sum
@@ -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=
-- 
2.33.1
Details
Message ID
<befe27e9-e203-0773-3265-00c36230f552@hirtz.pm>
In-Reply-To
<0101017d48f07fb7-ae157ba3-588c-42d4-a9ee-c0d6f70080d2-000000@us-west-2.amazonses.com> (view parent)
DKIM signature
pass
Download raw message
Pushed, thanks!
Reply to thread Export thread (mbox)