diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..5de2df8
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,18 @@
+# https://editorconfig.org/
+
+root = true
+
+[*]
+charset = utf-8
+insert_final_newline = true
+trim_trailing_whitespace = true
+indent_style = space
+indent_size = 2
+
+[*.go]
+indent_style = tab
+indent_size = 8
+
+[*.golden]
+insert_final_newline = false
+trim_trailing_whitespace = false
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..c19be87
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,38 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**Setup**
+Please complete the following information along with version numbers, if applicable.
+ - OS [e.g. Ubuntu, macOS]
+ - Shell [e.g. zsh, fish]
+ - Terminal Emulator [e.g. kitty, iterm]
+ - Terminal Multiplexer [e.g. tmux]
+ - Locale [e.g. en_US.UTF-8, zh_CN.UTF-8, etc.]
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Source Code**
+Please include source code if needed to reproduce the behavior.
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+Add screenshots to help explain your problem.
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..897a394
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: true
+contact_links:
+- name: Discord
+ url: https://charm.sh/discord
+ about: Chat on our Discord.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..11fc491
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: enhancement
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 9fc3b07..d944991 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,20 +1,57 @@
version: 2
+
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
- interval: "daily"
- labels:
- - "dependencies"
- commit-message:
- prefix: "feat"
- include: "scope"
- - package-ecosystem: "github-actions"
- directory: "/"
- schedule:
- interval: "daily"
+ interval: "weekly"
+ day: "monday"
+ time: "05:00"
+ timezone: "America/New_York"
labels:
- "dependencies"
commit-message:
prefix: "chore"
include: "scope"
+ groups:
+ all:
+ patterns:
+ - "*"
+ ignore:
+ - dependency-name: github.com/charmbracelet/bubbletea/v2
+ versions:
+ - v2.0.0-beta1
+
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ day: "monday"
+ time: "05:00"
+ timezone: "America/New_York"
+ labels:
+ - "dependencies"
+ commit-message:
+ prefix: "chore"
+ include: "scope"
+ groups:
+ all:
+ patterns:
+ - "*"
+
+ - package-ecosystem: "docker"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ day: "monday"
+ time: "05:00"
+ timezone: "America/New_York"
+ labels:
+ - "dependencies"
+ commit-message:
+ prefix: "chore"
+ include: "scope"
+ groups:
+ all:
+ patterns:
+ - "*"
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 20a7a22..adcb449 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -10,3 +10,16 @@ jobs:
uses: charmbracelet/meta/.github/workflows/snapshot.yml@main
secrets:
goreleaser_key: ${{ secrets.GORELEASER_KEY }}
+
+ govulncheck:
+ uses: charmbracelet/meta/.github/workflows/govulncheck.yml@main
+ with:
+ go-version: stable
+
+ semgrep:
+ uses: charmbracelet/meta/.github/workflows/semgrep.yml@main
+
+ ruleguard:
+ uses: charmbracelet/meta/.github/workflows/ruleguard.yml@main
+ with:
+ go-version: stable
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index bcc6721..7fbd40b 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -3,26 +3,22 @@ on: [push, pull_request]
jobs:
coverage:
- strategy:
- matrix:
- go-version: [^1]
- os: [ubuntu-latest]
- runs-on: ${{ matrix.os }}
+ runs-on: ubuntu-latest
env:
GO111MODULE: "on"
steps:
- - name: Install Go
- uses: actions/setup-go@v3
- with:
- go-version: ${{ matrix.go-version }}
-
- name: Checkout code
- uses: actions/checkout@v3
+ uses: actions/checkout@v6
+
+ - name: Install Go
+ uses: actions/setup-go@v6
+ with:
+ go-version: stable
- name: Coverage
env:
COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
go test -race -covermode atomic -coverprofile=profile.cov ./...
- GO111MODULE=off go get github.com/mattn/goveralls
- $(go env GOPATH)/bin/goveralls -coverprofile=profile.cov -service=github
+ go install github.com/mattn/goveralls@latest
+ goveralls -coverprofile=profile.cov -service=github
diff --git a/.github/workflows/dependabot-sync.yml b/.github/workflows/dependabot-sync.yml
new file mode 100644
index 0000000..9b08259
--- /dev/null
+++ b/.github/workflows/dependabot-sync.yml
@@ -0,0 +1,17 @@
+name: dependabot-sync
+on:
+ schedule:
+ - cron: "0 0 * * 0" # every Sunday at midnight
+ workflow_dispatch: # allows manual triggering
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ dependabot-sync:
+ uses: charmbracelet/meta/.github/workflows/dependabot-sync.yml@main
+ with:
+ repo_name: ${{ github.event.repository.name }}
+ secrets:
+ gh_token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml
index 1ccf980..fbc2046 100644
--- a/.github/workflows/goreleaser.yml
+++ b/.github/workflows/goreleaser.yml
@@ -1,3 +1,5 @@
+# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
+
name: goreleaser
on:
@@ -21,5 +23,3 @@ jobs:
nfpm_gpg_key: ${{ secrets.NFPM_GPG_KEY }}
nfpm_passphrase: ${{ secrets.NFPM_PASSPHRASE }}
snapcraft_token: ${{ secrets.SNAPCRAFT_TOKEN }}
-
-# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
diff --git a/.github/workflows/lint-sync.yml b/.github/workflows/lint-sync.yml
new file mode 100644
index 0000000..43f380f
--- /dev/null
+++ b/.github/workflows/lint-sync.yml
@@ -0,0 +1,13 @@
+name: lint-sync
+on:
+ schedule:
+ - cron: "0 0 * * 0" # every sunday at midnight
+ workflow_dispatch:
+
+permissions:
+ contents: write
+ pull-requests: write
+
+jobs:
+ lint:
+ uses: charmbracelet/meta/.github/workflows/lint-sync.yml@main
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index d0bb996..1e21125 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -8,9 +8,9 @@ jobs:
name: lint
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v6
- name: golangci-lint
- uses: golangci/golangci-lint-action@v3
+ uses: golangci/golangci-lint-action@v9.2.0
with:
# Optional: golangci-lint command line arguments.
args: --issues-exit-code=0
diff --git a/.github/workflows/soft-serve.yml b/.github/workflows/soft-serve.yml
deleted file mode 100644
index 8eb3221..0000000
--- a/.github/workflows/soft-serve.yml
+++ /dev/null
@@ -1,12 +0,0 @@
-name: soft-serve
-
-on:
- push:
- branches:
- - master
-
-jobs:
- soft-serve:
- uses: charmbracelet/meta/.github/workflows/soft-serve.yml@main
- secrets:
- ssh-key: "${{ secrets.CHARM_SOFT_SERVE_KEY }}"
diff --git a/.gitignore b/.gitignore
index a9d459b..8a5159a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,4 @@ glow
dist/
.envrc
completions/
-manpages/
\ No newline at end of file
+manpages/
diff --git a/.golangci.yml b/.golangci.yml
index 85cfb6d..929cb0a 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -1,34 +1,44 @@
+version: "2"
run:
tests: false
-
-issues:
- include:
- - EXC0001
- - EXC0005
- - EXC0011
- - EXC0012
- - EXC0013
-
- max-issues-per-linter: 0
- max-same-issues: 0
-
linters:
enable:
- bodyclose
- - dupl
- - exportloopref
+ - exhaustive
- goconst
- godot
- - godox
- - goimports
+ - gomoddirectives
- goprintffuncname
- gosec
- - ifshort
- misspell
+ - nakedret
+ - nestif
+ - nilerr
+ - noctx
+ - nolintlint
- prealloc
- revive
- rowserrcheck
- sqlclosecheck
+ - tparallel
- unconvert
- unparam
- whitespace
+ - wrapcheck
+ exclusions:
+ rules:
+ - text: '(slog|log)\.\w+'
+ linters:
+ - noctx
+ generated: lax
+ presets:
+ - common-false-positives
+issues:
+ max-issues-per-linter: 0
+ max-same-issues: 0
+formatters:
+ enable:
+ - gofumpt
+ - goimports
+ exclusions:
+ generated: lax
diff --git a/.goreleaser.yml b/.goreleaser.yml
index 6f6fead..0c539b7 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -1,14 +1,14 @@
+# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
+
+version: 2
+
includes:
- from_url:
url: charmbracelet/meta/main/goreleaser-glow.yaml
variables:
- main: "."
- binary_name: glow
- description: "Render markdown on the CLI, with pizzazz! 💅🏻"
- github_url: "https://github.com/charmbracelet/soft-serve"
+ description: "Render markdown on the CLI, with pizzazz!"
+ github_url: "https://github.com/charmbracelet/glow"
maintainer: "Christian Muehlhaeuser "
brew_commit_author_name: "Christian Muehlhaeuser"
brew_commit_author_email: "muesli@charm.sh"
-
-# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
diff --git a/LICENSE b/LICENSE
index 81c7cad..1e27a61 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2019-2022 Charmbracelet, Inc
+Copyright (c) 2019-2024 Charmbracelet, Inc
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 6d80864..0000000
--- a/Makefile
+++ /dev/null
@@ -1,20 +0,0 @@
-# This Makefile is just for development purposes
-
-.PHONY: default clean glow run log
-
-LOGFILE := debug.log
-
-default: glow
-
-clean:
- rm -f ./glow
-
-glow:
- go build
-
-run: clean glow
- GLOW_LOGFILE=$(LOGFILE) ./glow
-
-log:
- > $(LOGFILE)
- tail -f $(LOGFILE)
diff --git a/README.md b/README.md
index c36ed66..cf3bb15 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ Render markdown on the CLI, with _pizzazz_!
-
+
## What is it?
@@ -20,13 +20,9 @@ Glow is a terminal based markdown reader designed from the ground up to bring
out the beauty—and power—of the CLI.
Use it to discover markdown files, read documentation directly on the command
-line and stash markdown files to your own private collection, so you can read
-them anywhere. Glow will find local markdown files in subdirectories or a local
+line. Glow will find local markdown files in subdirectories or a local
Git repository.
-By the way, all data stashed is encrypted end-to-end: only you can decrypt it.
-More on that below.
-
## Installation
### Package Manager
@@ -34,38 +30,64 @@ More on that below.
```bash
# macOS or Linux
brew install glow
+```
+```bash
# macOS (with MacPorts)
sudo port install glow
+```
+```bash
# Arch Linux (btw)
pacman -S glow
+```
+```bash
# Void Linux
xbps-install -S glow
+```
-# Nix
-nix-env -iA nixpkgs.glow
+```bash
+# Nix shell
+nix-shell -p glow --command glow
+```
+```bash
# FreeBSD
pkg install glow
+```
+```bash
# Solus
eopkg install glow
+```
-# Windows (with Scoop or Chocolatey)
-scoop install glow
+```bash
+# Windows (with Chocolatey, Scoop, or Winget)
choco install glow
+scoop install glow
+winget install charmbracelet.glow
+```
+```bash
# Android (with termux)
pkg install glow
+```
+```bash
+# Ubuntu (Snapcraft)
+sudo snap install glow
+```
+
+```bash
# Debian/Ubuntu
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://repo.charm.sh/apt/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/charm.gpg
echo "deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *" | sudo tee /etc/apt/sources.list.d/charm.list
sudo apt update && sudo apt install glow
+```
+```bash
# Fedora/RHEL
echo '[charm]
name=Charm
@@ -83,11 +105,12 @@ packages. ARM builds are also available for macOS, Linux, FreeBSD and OpenBSD.
### Go
Or just install it with `go`:
+
```bash
-go install github.com/charmbracelet/glow@latest
+go install github.com/charmbracelet/glow/v2@latest
```
-### Build (requires Go 1.13+)
+### Build (requires Go 1.21+)
```bash
git clone https://github.com/charmbracelet/glow.git
@@ -97,11 +120,10 @@ go build
[releases]: https://github.com/charmbracelet/glow/releases
-
## The TUI
Simply run `glow` without arguments to start the textual user interface and
-browse local and stashed markdown. Glow will find local markdown files in the
+browse local. Glow will find local markdown files in the
current directory and below or, if you’re in a Git repository, Glow will search
the repo.
@@ -109,15 +131,6 @@ Markdown files can be read with Glow's high-performance pager. Most of the
keystrokes you know from `less` are the same, but you can press `?` to list
the hotkeys.
-### Stashing
-
-Glow works with the Charm Cloud to allow you to store any markdown files in
-your own private collection. You can stash a local document from the Glow TUI by
-pressing `s`.
-
-Stashing is private, its contents will not be exposed publicly, and it's
-encrypted end-to-end. More on encryption below.
-
## The CLI
In addition to a TUI, Glow has a CLI for working with Markdown. To format a
@@ -137,18 +150,6 @@ glow github.com/charmbracelet/glow
glow https://host.tld/file.md
```
-### Stashing
-
-You can also stash documents from the CLI:
-
-```bash
-glow stash README.md
-```
-
-Then, when you run `glow` without arguments will you can browse through your
-stashed documents. This is a great way to keep track of things that you need to
-reference often.
-
### Word Wrapping
The `-w` flag lets you set a maximum width at which the output will be wrapped:
@@ -200,42 +201,39 @@ Here's an example config:
```yaml
# style name or JSON path (default "auto")
style: "light"
-# show local files only; no network (TUI-mode only)
-local: true
-# mouse support (TUI-mode only)
+# mouse wheel support (TUI-mode only)
mouse: true
# use pager to display markdown
pager: true
-# word-wrap at width
+# at which column should we word wrap?
width: 80
+# show all files, including hidden and ignored.
+all: false
+# show line numbers (TUI-mode only)
+showLineNumbers: false
+# preserve newlines in the output
+preserveNewLines: false
```
-## 🔒 Encryption: How It Works
+## Contributing
-Encryption works by issuing symmetric keys (basically a generated password) and
-encrypting it with the local SSH public key generated by the open-source
-[charm][charmlib] library. That encrypted key is then sent up to our server.
-We can’t read it since we don’t have your private key. When you want to decrypt
-something or view your stash, that key is downloaded from our server and
-decrypted locally using the SSH private key. When you link accounts, the
-symmetric key is encrypted for each new public key. This happens on your
-machine and not our server, so we never see any unencrypted data.
+See [contributing][contribute].
-[charmlib]: https://github.com/charmbracelet/charm
+[contribute]: https://github.com/charmbracelet/glow/contribute
## Feedback
We’d love to hear your thoughts on this project. Feel free to drop us a note!
-* [Twitter](https://twitter.com/charmcli)
-* [The Fediverse](https://mastodon.social/@charmcli)
-* [Discord](https://charm.sh/chat)
+- [Twitter](https://twitter.com/charmcli)
+- [The Fediverse](https://mastodon.social/@charmcli)
+- [Discord](https://charm.sh/chat)
## License
[MIT](https://github.com/charmbracelet/glow/raw/master/LICENSE)
-***
+---
Part of [Charm](https://charm.sh).
diff --git a/Taskfile.yaml b/Taskfile.yaml
new file mode 100644
index 0000000..6d8c844
--- /dev/null
+++ b/Taskfile.yaml
@@ -0,0 +1,23 @@
+# https://taskfile.dev
+
+version: '3'
+
+tasks:
+ lint:
+ desc: Run base linters
+ cmds:
+ - golangci-lint run
+
+ test:
+ desc: Run tests
+ cmds:
+ - go test ./... {{.CLI_ARGS}}
+
+ log:
+ desc: Watch for glow logs
+ aliases: [tail]
+ cmds:
+ - cmd: tail -f ~/Library/Caches/glow/glow.log
+ platforms: [darwin]
+ - cmd: tail -f ~/.cache/glow/glow.log
+ platforms: [linux, windows]
diff --git a/config_cmd.go b/config_cmd.go
index 6dfc835..390a1db 100644
--- a/config_cmd.go
+++ b/config_cmd.go
@@ -3,26 +3,27 @@ package main
import (
"errors"
"fmt"
+ "io/fs"
"os"
- "os/exec"
"path"
"path/filepath"
- "strings"
- gap "github.com/muesli/go-app-paths"
+ "github.com/charmbracelet/x/editor"
"github.com/spf13/cobra"
+ "github.com/spf13/viper"
)
const defaultConfig = `# style name or JSON path (default "auto")
style: "auto"
-# show local files only; no network (TUI-mode only)
-local: false
# mouse support (TUI-mode only)
mouse: false
# use pager to display markdown
pager: false
# word-wrap at width
-width: 80`
+width: 80
+# show all files, including hidden and ignored.
+all: false
+`
var configCmd = &cobra.Command{
Use: "config",
@@ -31,59 +32,57 @@ var configCmd = &cobra.Command{
Long: paragraph(fmt.Sprintf("\n%s the glow config file. We’ll use EDITOR to determine which editor to use. If the config file doesn't exist, it will be created.", keyword("Edit"))),
Example: paragraph("glow config\nglow config --config path/to/config.yml"),
Args: cobra.NoArgs,
- RunE: func(cmd *cobra.Command, args []string) error {
- editor := strings.Fields(os.Getenv("EDITOR"))
- if len(editor) == 0 {
- return errors.New("no EDITOR environment variable set")
- }
-
- if configFile == "" {
- scope := gap.NewScope(gap.User, "glow")
-
- var err error
- configFile, err = scope.ConfigPath("glow.yml")
- if err != nil {
- return err
- }
- }
-
- if ext := path.Ext(configFile); ext != ".yaml" && ext != ".yml" {
- return fmt.Errorf("'%s' is not a supported config type: use '%s' or '%s'", ext, ".yaml", ".yml")
- }
-
- if _, err := os.Stat(configFile); os.IsNotExist(err) {
- // File doesn't exist yet, create all necessary directories and
- // write the default config file
- if err := os.MkdirAll(filepath.Dir(configFile), 0o700); err != nil {
- return err
- }
-
- f, err := os.Create(configFile)
- if err != nil {
- return err
- }
- defer func() { _ = f.Close() }()
-
- if _, err := f.WriteString(defaultConfig); err != nil {
- return err
- }
- } else if err != nil { // some other error occurred
+ RunE: func(*cobra.Command, []string) error {
+ if err := ensureConfigFile(); err != nil {
return err
}
- var eargs []string
- if len(editor) > 1 {
- eargs = editor[1:]
+ c, err := editor.Cmd("Glow", configFile)
+ if err != nil {
+ return fmt.Errorf("unable to set config file: %w", err)
}
- c := exec.Command(editor[0], append(eargs, configFile)...) // nolint: gosec
c.Stdin = os.Stdin
c.Stdout = os.Stdout
c.Stderr = os.Stderr
if err := c.Run(); err != nil {
- return err
+ return fmt.Errorf("unable to run command: %w", err)
}
fmt.Println("Wrote config file to:", configFile)
return nil
},
}
+
+func ensureConfigFile() error {
+ if configFile == "" {
+ configFile = viper.GetViper().ConfigFileUsed()
+ if err := os.MkdirAll(filepath.Dir(configFile), 0o755); err != nil { //nolint:gosec
+ return fmt.Errorf("could not write configuration file: %w", err)
+ }
+ }
+
+ if ext := path.Ext(configFile); ext != ".yaml" && ext != ".yml" {
+ return fmt.Errorf("'%s' is not a supported configuration type: use '%s' or '%s'", ext, ".yaml", ".yml")
+ }
+
+ if _, err := os.Stat(configFile); errors.Is(err, fs.ErrNotExist) {
+ // File doesn't exist yet, create all necessary directories and
+ // write the default config file
+ if err := os.MkdirAll(filepath.Dir(configFile), 0o700); err != nil {
+ return fmt.Errorf("unable create directory: %w", err)
+ }
+
+ f, err := os.Create(configFile)
+ if err != nil {
+ return fmt.Errorf("unable to create config file: %w", err)
+ }
+ defer func() { _ = f.Close() }()
+
+ if _, err := f.WriteString(defaultConfig); err != nil {
+ return fmt.Errorf("unable to write config file: %w", err)
+ }
+ } else if err != nil { // some other error occurred
+ return fmt.Errorf("unable to stat config file: %w", err)
+ }
+ return nil
+}
diff --git a/github.go b/github.go
index b824cf5..fe862e3 100644
--- a/github.go
+++ b/github.go
@@ -1,47 +1,55 @@
package main
import (
+ "encoding/json"
"errors"
+ "fmt"
+ "io"
"net/http"
"net/url"
"strings"
)
-// isGitHubURL tests a string to determine if it is a well-structured GitHub URL.
-func isGitHubURL(s string) (string, bool) {
- if strings.HasPrefix(s, "github.com/") {
- s = "https://" + s
+// findGitHubREADME tries to find the correct README filename in a repository using GitHub API.
+func findGitHubREADME(u *url.URL) (*source, error) {
+ owner, repo, ok := strings.Cut(strings.TrimPrefix(u.Path, "/"), "/")
+ if !ok {
+ return nil, fmt.Errorf("invalid url: %s", u.String())
}
- u, err := url.ParseRequestURI(s)
+ type readme struct {
+ DownloadURL string `json:"download_url"`
+ }
+
+ apiURL := fmt.Sprintf("https://api.%s/repos/%s/%s/readme", u.Hostname(), owner, repo)
+
+ //nolint:bodyclose
+ // it is closed on the caller
+ res, err := http.Get(apiURL) //nolint: gosec,noctx
if err != nil {
- return "", false
+ return nil, fmt.Errorf("unable to get url: %w", err)
}
- return u.String(), strings.ToLower(u.Host) == "github.com"
-}
-
-// findGitHubREADME tries to find the correct README filename in a repository.
-func findGitHubREADME(s string) (*source, error) {
- u, err := url.ParseRequestURI(s)
+ body, err := io.ReadAll(res.Body)
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("unable to read http response body: %w", err)
}
- u.Host = "raw.githubusercontent.com"
- for _, r := range readmeNames {
- v := u
- v.Path += "/master/" + r
+ var result readme
+ if err := json.Unmarshal(body, &result); err != nil {
+ return nil, fmt.Errorf("unable to parse json: %w", err)
+ }
- // nolint:bodyclose
+ if res.StatusCode == http.StatusOK {
+ //nolint:bodyclose
// it is closed on the caller
- resp, err := http.Get(v.String())
+ resp, err := http.Get(result.DownloadURL) //nolint: noctx
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("unable to get url: %w", err)
}
if resp.StatusCode == http.StatusOK {
- return &source{resp.Body, v.String()}, nil
+ return &source{resp.Body, result.DownloadURL}, nil
}
}
diff --git a/gitlab.go b/gitlab.go
index e8c8a57..68256be 100644
--- a/gitlab.go
+++ b/gitlab.go
@@ -1,46 +1,59 @@
package main
import (
+ "encoding/json"
"errors"
+ "fmt"
+ "io"
"net/http"
"net/url"
"strings"
)
-// isGitLabURL tests a string to determine if it is a well-structured GitLab URL.
-func isGitLabURL(s string) (string, bool) {
- if strings.HasPrefix(s, "gitlab.com/") {
- s = "https://" + s
+// findGitLabREADME tries to find the correct README filename in a repository using GitLab API.
+func findGitLabREADME(u *url.URL) (*source, error) {
+ owner, repo, ok := strings.Cut(strings.TrimPrefix(u.Path, "/"), "/")
+ if !ok {
+ return nil, fmt.Errorf("invalid url: %s", u.String())
}
- u, err := url.ParseRequestURI(s)
+ projectPath := url.QueryEscape(owner + "/" + repo)
+
+ type readme struct {
+ ReadmeURL string `json:"readme_url"`
+ }
+
+ apiURL := fmt.Sprintf("https://%s/api/v4/projects/%s", u.Hostname(), projectPath)
+
+ //nolint:bodyclose
+ // it is closed on the caller
+ res, err := http.Get(apiURL) //nolint: gosec,noctx
if err != nil {
- return "", false
+ return nil, fmt.Errorf("unable to get url: %w", err)
}
- return u.String(), strings.ToLower(u.Host) == "gitlab.com"
-}
-
-// findGitLabREADME tries to find the correct README filename in a repository.
-func findGitLabREADME(s string) (*source, error) {
- u, err := url.ParseRequestURI(s)
+ body, err := io.ReadAll(res.Body)
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("unable to read http response body: %w", err)
}
- for _, r := range readmeNames {
- v := u
- v.Path += "/raw/master/" + r
+ var result readme
+ if err := json.Unmarshal(body, &result); err != nil {
+ return nil, fmt.Errorf("unable to parse json: %w", err)
+ }
- // nolint:bodyclose
+ readmeRawURL := strings.ReplaceAll(result.ReadmeURL, "blob", "raw")
+
+ if res.StatusCode == http.StatusOK {
+ //nolint:bodyclose
// it is closed on the caller
- resp, err := http.Get(v.String())
+ resp, err := http.Get(readmeRawURL) //nolint: gosec,noctx
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("unable to get url: %w", err)
}
if resp.StatusCode == http.StatusOK {
- return &source{resp.Body, v.String()}, nil
+ return &source{resp.Body, readmeRawURL}, nil
}
}
diff --git a/glow_test.go b/glow_test.go
index ed4f0cd..8743be2 100644
--- a/glow_test.go
+++ b/glow_test.go
@@ -1,31 +1,9 @@
package main
import (
- "bytes"
"testing"
)
-func TestGlowSources(t *testing.T) {
- tt := []string{
- ".",
- "README.md",
- "github.com/charmbracelet/glow",
- "https://github.com/charmbracelet/glow",
- }
-
- for _, v := range tt {
- buf := &bytes.Buffer{}
- err := executeArg(rootCmd, v, buf)
-
- if err != nil {
- t.Errorf("Error during execution (args: %s): %v", v, err)
- }
- if buf.Len() == 0 {
- t.Errorf("Output buffer should not be empty (args: %s)", v)
- }
- }
-}
-
func TestGlowFlags(t *testing.T) {
tt := []struct {
args []string
diff --git a/go.mod b/go.mod
index 934d472..6e05b0d 100644
--- a/go.mod
+++ b/go.mod
@@ -1,27 +1,75 @@
-module github.com/charmbracelet/glow
+module github.com/charmbracelet/glow/v2
-go 1.16
+go 1.25.12
+
+toolchain go1.26.5
require (
github.com/atotto/clipboard v0.1.4
- github.com/charmbracelet/bubbles v0.15.0
- github.com/charmbracelet/bubbletea v0.23.1
- github.com/charmbracelet/charm v0.8.6
- github.com/charmbracelet/glamour v0.6.0
- github.com/charmbracelet/lipgloss v0.6.0
- github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac
- github.com/mattn/go-runewidth v0.0.14
- github.com/meowgorithm/babyenv v1.3.1
+ github.com/caarlos0/env/v11 v11.3.1
+ github.com/charmbracelet/bubbles v0.21.0
+ github.com/charmbracelet/bubbletea v1.3.10
+ github.com/charmbracelet/glamour v0.10.0
+ github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
+ github.com/charmbracelet/log v0.4.2
+ github.com/charmbracelet/x/editor v0.1.0
+ github.com/dustin/go-humanize v1.0.1
+ github.com/fsnotify/fsnotify v1.9.0
+ github.com/mattn/go-runewidth v0.0.19
github.com/mitchellh/go-homedir v1.1.0
- github.com/muesli/gitcha v0.2.0
+ github.com/muesli/gitcha v0.3.0
github.com/muesli/go-app-paths v0.2.2
+ github.com/muesli/mango-cobra v1.3.0
github.com/muesli/reflow v0.3.0
- github.com/muesli/termenv v0.13.0
- github.com/sahilm/fuzzy v0.1.0
- github.com/segmentio/ksuid v1.0.4
- github.com/spf13/cobra v1.6.1
- github.com/spf13/viper v1.14.0
- golang.org/x/sys v0.0.0-20220908164124-27713097b956
- golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
- golang.org/x/text v0.6.0
+ github.com/muesli/roff v0.1.0
+ github.com/muesli/termenv v0.16.0
+ github.com/sahilm/fuzzy v0.1.1
+ github.com/spf13/cobra v1.10.2
+ github.com/spf13/viper v1.21.0
+ golang.org/x/sys v0.43.0
+ golang.org/x/term v0.42.0
+ golang.org/x/text v0.39.0
+ mvdan.cc/sh/v3 v3.13.1
+)
+
+require (
+ github.com/alecthomas/chroma/v2 v2.14.0 // indirect
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
+ github.com/aymerick/douceur v0.2.0 // indirect
+ github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
+ github.com/charmbracelet/x/ansi v0.10.1 // indirect
+ github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
+ github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
+ github.com/charmbracelet/x/term v0.2.1 // indirect
+ github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
+ github.com/dlclark/regexp2 v1.11.0 // indirect
+ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
+ github.com/go-logfmt/logfmt v0.6.0 // indirect
+ github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
+ github.com/gorilla/css v1.0.1 // indirect
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-localereader v0.0.1 // indirect
+ github.com/microcosm-cc/bluemonday v1.0.27 // indirect
+ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
+ github.com/muesli/cancelreader v0.2.2 // indirect
+ github.com/muesli/mango v0.2.0 // indirect
+ github.com/muesli/mango-pflag v0.1.0 // indirect
+ github.com/pelletier/go-toml/v2 v2.2.4 // indirect
+ github.com/rivo/uniseg v0.4.7 // indirect
+ github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94 // indirect
+ github.com/sagikazarmark/locafero v0.11.0 // indirect
+ github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
+ github.com/spf13/afero v1.15.0 // indirect
+ github.com/spf13/cast v1.10.0 // indirect
+ github.com/spf13/pflag v1.0.10 // indirect
+ github.com/subosito/gotenv v1.6.0 // indirect
+ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
+ github.com/yuin/goldmark v1.7.17 // indirect
+ github.com/yuin/goldmark-emoji v1.0.5 // indirect
+ go.yaml.in/yaml/v3 v3.0.4 // indirect
+ golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 // indirect
+ golang.org/x/net v0.53.0 // indirect
+ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)
diff --git a/go.sum b/go.sum
index f59cdb9..0ccd7cd 100644
--- a/go.sum
+++ b/go.sum
@@ -1,1254 +1,177 @@
-cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
-cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
-cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
-cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
-cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
-cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
-cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
-cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
-cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
-cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
-cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
-cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
-cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
-cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
-cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
-cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
-cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
-cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
-cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
-cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
-cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
-cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
-cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
-cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
-cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
-cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
-cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
-cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
-cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
-cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc=
-cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU=
-cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA=
-cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw=
-cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY=
-cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI=
-cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4=
-cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4=
-cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0=
-cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ=
-cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk=
-cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o=
-cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s=
-cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0=
-cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY=
-cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw=
-cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI=
-cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0=
-cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8=
-cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
-cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
-cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
-cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
-cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
-cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
-cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA=
-cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY=
-cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s=
-cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM=
-cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI=
-cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY=
-cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI=
-cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=
-cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=
-cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=
-cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s=
-cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU=
-cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U=
-cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU=
-cloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=
-cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=
-cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU=
-cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
-cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I=
-cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4=
-cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0=
-cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs=
-cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc=
-cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM=
-cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ=
-cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo=
-cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE=
-cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I=
-cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ=
-cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo=
-cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA=
-cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
-cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
-cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo=
-cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ=
-cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4=
-cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0=
-cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8=
-cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU=
-cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU=
-cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y=
-cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg=
-cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk=
-cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w=
-cloud.google.com/go/firestore v1.8.0/go.mod h1:r3KB8cAdRIe8znzoPWLw8S6gpDVd9treohhn8b09424=
-cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk=
-cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg=
-cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM=
-cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA=
-cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o=
-cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A=
-cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0=
-cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0=
-cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc=
-cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY=
-cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic=
-cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI=
-cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8=
-cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08=
-cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4=
-cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w=
-cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE=
-cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM=
-cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY=
-cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s=
-cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA=
-cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o=
-cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ=
-cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU=
-cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY=
-cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34=
-cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs=
-cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg=
-cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E=
-cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU=
-cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0=
-cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA=
-cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0=
-cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI=
-cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
-cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
-cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
-cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
-cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4=
-cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o=
-cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk=
-cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo=
-cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg=
-cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4=
-cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg=
-cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c=
-cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y=
-cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A=
-cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4=
-cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY=
-cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s=
-cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI=
-cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA=
-cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4=
-cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0=
-cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU=
-cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU=
-cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc=
-cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs=
-cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg=
-cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM=
-cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ=
-cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
-cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
-cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
-cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
-cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
-cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
-cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y=
-cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc=
-cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw=
-cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g=
-cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU=
-cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4=
-cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0=
-cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo=
-cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo=
-cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE=
-cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg=
-cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0=
-cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M=
-dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
-github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
-github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
-github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
-github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
-github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
-github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
-github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
-github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
-github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
-github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
-github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
-github.com/armon/go-metrics v0.4.0/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
-github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
-github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
-github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
+github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
+github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
+github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
+github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
+github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
+github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
-github.com/aymanbagabas/go-osc52 v1.0.3 h1:DTwqENW7X9arYimJrPeGZcV0ln14sGMt3pHZspWD+Mg=
-github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
+github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
-github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
-github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
-github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
-github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
-github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
-github.com/calmh/randomart v1.1.0 h1:evl+iwc10LXtHdMZhzLxmsCQVmWnkXs44SbC6Uk0Il8=
-github.com/calmh/randomart v1.1.0/go.mod h1:DQUbPVyP+7PAs21w/AnfMKG5NioxS3TbZ2F9MSK/jFM=
-github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
-github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
-github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/charmbracelet/bubbles v0.7.5/go.mod h1:IRTORFvhEI6OUH7WhN2Ks8Z8miNGimk1BE6cmHijOkM=
-github.com/charmbracelet/bubbles v0.15.0 h1:c5vZ3woHV5W2b8YZI1q7v4ZNQaPetfHuoHzx+56Z6TI=
-github.com/charmbracelet/bubbles v0.15.0/go.mod h1:Y7gSFbBzlMpUDR/XM9MhZI374Q+1p1kluf1uLl8iK74=
-github.com/charmbracelet/bubbletea v0.12.2/go.mod h1:3gZkYELUOiEUOp0bTInkxguucy/xRbGSOcbMs1geLxg=
-github.com/charmbracelet/bubbletea v0.23.1 h1:CYdteX1wCiCzKNUlwm25ZHBIc1GXlYFyUIte8WPvhck=
-github.com/charmbracelet/bubbletea v0.23.1/go.mod h1:JAfGK/3/pPKHTnAS8JIE2u9f61BjWTQY57RbT25aMXU=
-github.com/charmbracelet/charm v0.8.6 h1:/U6rxGj4J6zZ1Ex8+wTr4hNMr4ESBzNZbC1UyrJPVbg=
-github.com/charmbracelet/charm v0.8.6/go.mod h1:8dE3uX+TYSpa7Q6e/CmjN6WSd7koSAKNQTGWugFREx4=
-github.com/charmbracelet/glamour v0.6.0 h1:wi8fse3Y7nfcabbbDuwolqTqMQPMnVPeZhDM273bISc=
-github.com/charmbracelet/glamour v0.6.0/go.mod h1:taqWV4swIMMbWALc0m7AfE9JkPSU8om2538k9ITBxOc=
-github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
-github.com/charmbracelet/lipgloss v0.6.0 h1:1StyZB9vBSOyuZxQUcUwGr17JmojPNm87inij9N3wJY=
-github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk=
-github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
-github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
-github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
-github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
-github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
-github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
-github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
-github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
-github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw=
-github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
-github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
-github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
-github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
-github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
-github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
-github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
-github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
-github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
-github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
-github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
+github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
+github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
+github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
+github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
+github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
+github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
+github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
+github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY=
+github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
+github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
+github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
+github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
+github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
+github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
+github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
+github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
+github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
+github.com/charmbracelet/x/editor v0.1.0 h1:p69/dpvlwRTs9uYiPeAWruwsHqTFzHhTvQOd/WVSX98=
+github.com/charmbracelet/x/editor v0.1.0/go.mod h1:oivrEbcP/AYt/Hpvk5pwDXXrQ933gQS6UzL6fxqAGSA=
+github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
+github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
+github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=
+github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=
+github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
+github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
+github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
+github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
-github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
-github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
-github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
-github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
-github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
-github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac h1:opbrjaN/L8gg6Xh5D04Tem+8xVcz6ajZlGCs49mQgyg=
-github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
-github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
-github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
-github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
-github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
-github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
-github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
-github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
-github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
-github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
-github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
-github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
-github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
-github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
-github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
-github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
-github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
-github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
-github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
-github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
-github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
-github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
-github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
-github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
-github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
-github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
-github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
-github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
-github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
-github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
-github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
-github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
-github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
-github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
-github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
-github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
-github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
-github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
-github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
-github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4=
-github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
-github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
-github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
-github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
-github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
-github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
-github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
-github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8=
-github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
-github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
-github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
-github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
-github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
-github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=
-github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
-github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c=
-github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo=
-github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY=
-github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
-github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
-github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
-github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
-github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
-github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
-github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
-github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
-github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
-github.com/hashicorp/consul/api v1.15.3/go.mod h1:/g/qgcoBcEXALCNZgRRisyTW0nY86++L0KbeAMXYCeY=
-github.com/hashicorp/consul/sdk v0.11.0/go.mod h1:yPkX5Q6CsxTFMjQQDJwzeNmUUF5NUGGbrDsv9wTb8cw=
-github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
-github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
-github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
-github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
-github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
-github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
-github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
-github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
-github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
-github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
-github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
-github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
-github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
-github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
-github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
-github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
-github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
-github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
-github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
-github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
-github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
-github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
-github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
-github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
-github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
-github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
-github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
-github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
-github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
-github.com/hashicorp/memberlist v0.3.1/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
-github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
-github.com/hashicorp/serf v0.9.8/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
-github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
-github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
-github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
-github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
-github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
-github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
-github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
-github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
-github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
-github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
-github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
-github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
-github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
-github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
-github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
+github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
+github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
+github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
+github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
+github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
+github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
+github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
+github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
+github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
+github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
+github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
-github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
-github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
-github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
-github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
-github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
-github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
-github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
-github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
-github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
-github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
-github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
-github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
-github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
-github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
-github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
-github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
-github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
-github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
-github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
-github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
-github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
-github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
-github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
-github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
-github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
-github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
-github.com/meowgorithm/babyenv v1.3.0/go.mod h1:lwNX+J6AGBFqNrMZ2PTLkM6SO+W4X8DOg9zBDO4j3Ig=
-github.com/meowgorithm/babyenv v1.3.1 h1:18ZEYIgbzoFQfRLF9+lxjRfk/ui6w8U0FWl07CgWvvc=
-github.com/meowgorithm/babyenv v1.3.1/go.mod h1:lwNX+J6AGBFqNrMZ2PTLkM6SO+W4X8DOg9zBDO4j3Ig=
-github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg=
-github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=
-github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
-github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
-github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a h1:eU8j/ClY2Ty3qdHnn0TyW3ivFoPC/0F1gQZz8yTxbbE=
-github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a/go.mod h1:v8eSC2SMp9/7FTKUncp7fH9IwPfw+ysMObcEz5FWheQ=
-github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
-github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
+github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
+github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
+github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
+github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
-github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
-github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
-github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
-github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
-github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
-github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
-github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
-github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
-github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
-github.com/muesli/gitcha v0.2.0 h1:+wOgT2dI9s2Tznj1t1rb/qkK5e0cb6qD8c4IX2TR/YY=
-github.com/muesli/gitcha v0.2.0/go.mod h1:Ri8m9TZS4+ORG4JVmVKUQcWZuxDvUW3UKxMdQfzG2zI=
-github.com/muesli/go-app-paths v0.2.1/go.mod h1:SxS3Umca63pcFcLtbjVb+J0oD7cl4ixQWoBKhGEtEho=
+github.com/muesli/gitcha v0.3.0 h1:+PJkVKrDXVB0VgRn/yVx2CqSVSDGMSepzvohsCrPYtQ=
+github.com/muesli/gitcha v0.3.0/go.mod h1:vX3jFL+XcEUq1uY74RCjLSZfAV+ZuvLg70/NGPdXn84=
github.com/muesli/go-app-paths v0.2.2 h1:NqG4EEZwNIhBq/pREgfBmgDmt3h1Smr1MjZiXbpZUnI=
github.com/muesli/go-app-paths v0.2.2/go.mod h1:SxS3Umca63pcFcLtbjVb+J0oD7cl4ixQWoBKhGEtEho=
-github.com/muesli/reflow v0.1.0/go.mod h1:I9bWAt7QTg/que/qmUCJBGlj7wEq8OAFBjPNjc6xK4I=
-github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
+github.com/muesli/mango v0.2.0 h1:iNNc0c5VLQ6fsMgAqGQofByNUBH2Q2nEbD6TaI+5yyQ=
+github.com/muesli/mango v0.2.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4=
+github.com/muesli/mango-cobra v1.3.0 h1:vQy5GvPg3ndOSpduxutqFoINhWk3vD5K2dXo5E8pqec=
+github.com/muesli/mango-cobra v1.3.0/go.mod h1:Cj1ZrBu3806Qw7UjxnAUgE+7tllUBj1NCLQDwwGx19E=
+github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe7Sg=
+github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
-github.com/muesli/sasquatch v0.0.0-20200811221207-66979d92330a h1:Hw/15RYEOUD6T9UCRkUmNBa33kJkH33Fui6hE4sRLKU=
-github.com/muesli/sasquatch v0.0.0-20200811221207-66979d92330a/go.mod h1:+XG0ne5zXWBTSbbe7Z3/RWxaT8PZY6zaZ1dX6KjprYY=
-github.com/muesli/termenv v0.7.2/go.mod h1:ct2L5N2lmix82RaY3bMWwVu/jUFc9Ule0KGDCiKYPh8=
-github.com/muesli/termenv v0.7.4/go.mod h1:pZ7qY9l3F7e5xsAOS0zCew2tME+p7bWeBkotCEcIIcc=
-github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
-github.com/muesli/termenv v0.13.0 h1:wK20DRpJdDX8b7Ek2QfhvqhRQFZ237RGRO0RQ/Iqdy0=
-github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc=
-github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
-github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
-github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
-github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
-github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
-github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
-github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
-github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
-github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg=
-github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas=
-github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
+github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8=
+github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig=
+github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
+github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
+github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
+github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
-github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
-github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
-github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
-github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
-github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
-github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
-github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
-github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
-github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
-github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
-github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
-github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
-github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
-github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
-github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
-github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
-github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
-github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
-github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
-github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
-github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
-github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
-github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
-github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
-github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
-github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
-github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
-github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
-github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94 h1:G04eS0JkAIVZfaJLjla9dNxkJCPiKIGZlw9AfOhzOD0=
github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94/go.mod h1:b18R55ulyQ/h3RaWyloPyER7fWQVZvimKKhnI5OfrJQ=
-github.com/sagikazarmark/crypt v0.8.0/go.mod h1:TmKwZAo97S4Fy4sfMH/HX/cQP5D+ijra2NyLpNNmttY=
-github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
-github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
-github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
-github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c=
-github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
-github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
-github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
-github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
-github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
-github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
-github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
-github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
-github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw=
-github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
-github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
-github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
-github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
-github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
-github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
-github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
-github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
-github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
-github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
-github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
-github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
-github.com/spf13/viper v1.14.0 h1:Rg7d3Lo706X9tHsJMUjdiwMpHB7W8WnSVOssIY+JElU=
-github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As=
+github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
+github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
+github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
+github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
+github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
+github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
+github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
+github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
+github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
+github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
+github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
+github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
+github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
+github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
+github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
-github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
-github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
-github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
-github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
-github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
-github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
-github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
-github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
-github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
-github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
-github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-github.com/yuin/goldmark v1.5.2 h1:ALmeCk/px5FSm1MAcFBAsVKZjDuMVj8Tm7FFIlMJnqU=
-github.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os=
-github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ=
-go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
-go.etcd.io/etcd/api/v3 v3.5.5/go.mod h1:KFtNaxGDw4Yx/BA4iPPwevUTAuqcsPxzyX8PHydchN8=
-go.etcd.io/etcd/client/pkg/v3 v3.5.5/go.mod h1:ggrwbk069qxpKPq8/FKkQ3Xq9y39kbFR4LnKszpRXeQ=
-go.etcd.io/etcd/client/v2 v2.305.5/go.mod h1:zQjKllfqfBVyVStbt4FaosoX2iYd8fV/GRy/PbowgP4=
-go.etcd.io/etcd/client/v3 v3.5.5/go.mod h1:aApjR4WGlSumpnJ2kloS75h6aHUmAyaPLjHMxpc7E7c=
-go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
-go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
-go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
-go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
-go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
-go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
-go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
-go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
-go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
-go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
-go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
-go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
-go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
-go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
-go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
-golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
-golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=
-golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
-golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
-golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
-golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
-golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
-golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
-golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
-golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
-golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
-golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
-golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
-golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
-golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
-golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
-golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
-golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
-golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
-golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
-golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
-golang.org/x/net v0.0.0-20221014081412-f15817d10f9b h1:tvrvnPFcdzp294diPnrdZZZ8XUt2Tyj7svb7X52iDuU=
-golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
-golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
-golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
-golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
-golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
-golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
-golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
-golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
-golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201020230747-6e5568b54d1a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220908164124-27713097b956 h1:XeJjHH1KiLpKGb6lvMiksZ9l0fVUh+AmGcm0nOMEBOY=
-golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
-golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
-golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
-golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
-golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
-golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
-golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
-golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
-golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
-golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
-google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
-google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
-google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
-google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
-google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
-google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
-google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
-google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
-google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
-google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
-google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
-google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
-google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
-google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
-google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
-google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
-google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
-google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
-google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
-google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
-google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=
-google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g=
-google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA=
-google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8=
-google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs=
-google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
-google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
-google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=
-google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg=
-google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o=
-google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g=
-google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
-google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw=
-google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI=
-google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
-google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
-google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s=
-google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70=
-google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo=
-google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
-google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
-google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
-google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
-google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
-google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
-google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
-google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
-google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
-google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
-google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
-google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
-google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
-google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
-google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
-google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
-google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
-google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
-google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
-google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
-google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
-google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
-google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
-google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=
-google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
-google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
-google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
-google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
-google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
-google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
-google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
-google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
-google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
-google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
-google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
-google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
-google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
-google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
-google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE=
-google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc=
-google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
-google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
-google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
-google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
-google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk=
-google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=
-google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=
-google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=
-google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=
-google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo=
-google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw=
-google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI=
-google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI=
-google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U=
-google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM=
-google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM=
-google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s=
-google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
-google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
-google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
-google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
-google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
-google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
-google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
-google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
-google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
-google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
-google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
-google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
-google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
-google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
-google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k=
-google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
-google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
-google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
-google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
-google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
-google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
-google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
-google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
-google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
-google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
-google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
-google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
-google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
-google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
-google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
-google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
-google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
-google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
-google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
+github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
+github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
+github.com/yuin/goldmark v1.7.17 h1:p36OVWwRb246iHxA/U4p8OPEpOTESm4n+g+8t0EE5uA=
+github.com/yuin/goldmark v1.7.17/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
+github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
+github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
+go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
+golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 h1:LoYXNGAShUG3m/ehNk4iFctuhGX/+R1ZpfJ4/ia80JM=
+golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
+golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
+golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
+golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
+golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
+golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
+golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
-gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
-gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
-gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
-gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
-gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
-honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
-rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
-rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
-sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
+mvdan.cc/sh/v3 v3.13.1 h1:DP3TfgZhDkT7lerUdnp6PTGKyxxzz6T+cOlY/xEvfWk=
+mvdan.cc/sh/v3 v3.13.1/go.mod h1:lXJ8SexMvEVcHCoDvAGLZgFJ9Wsm2sulmoNEXGhYZD0=
diff --git a/log.go b/log.go
new file mode 100644
index 0000000..a864a71
--- /dev/null
+++ b/log.go
@@ -0,0 +1,40 @@
+package main
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+
+ "github.com/charmbracelet/log"
+ gap "github.com/muesli/go-app-paths"
+)
+
+func getLogFilePath() (string, error) {
+ dir, err := gap.NewScope(gap.User, "glow").CacheDir()
+ if err != nil {
+ return "", fmt.Errorf("unable to get cache dir: %w", err)
+ }
+ return filepath.Join(dir, "glow.log"), nil
+}
+
+func setupLog() (func() error, error) {
+ log.SetOutput(io.Discard)
+ // Log to file, if set
+ logFile, err := getLogFilePath()
+ if err != nil {
+ return nil, err
+ }
+ if err := os.MkdirAll(filepath.Dir(logFile), 0o755); err != nil { //nolint:gosec
+ // log disabled
+ return func() error { return nil }, nil //nolint:nilerr
+ }
+ f, err := os.OpenFile(logFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o644) //nolint:gosec
+ if err != nil {
+ // log disabled
+ return func() error { return nil }, nil //nolint:nilerr
+ }
+ log.SetOutput(f)
+ log.SetLevel(log.DebugLevel)
+ return f.Close, nil
+}
diff --git a/main.go b/main.go
index 17d66e7..b31ca15 100644
--- a/main.go
+++ b/main.go
@@ -1,9 +1,11 @@
+// Package main provides the entry point for the Glow CLI application.
package main
import (
"errors"
"fmt"
"io"
+ "io/fs"
"net/http"
"net/url"
"os"
@@ -11,11 +13,15 @@ import (
"path/filepath"
"strings"
- tea "github.com/charmbracelet/bubbletea"
+ "mvdan.cc/sh/v3/shell"
+
+ "github.com/caarlos0/env/v11"
"github.com/charmbracelet/glamour"
- "github.com/charmbracelet/glow/ui"
- "github.com/charmbracelet/glow/utils"
- "github.com/meowgorithm/babyenv"
+ "github.com/charmbracelet/glamour/styles"
+ "github.com/charmbracelet/glow/v2/ui"
+ "github.com/charmbracelet/glow/v2/utils"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/charmbracelet/log"
gap "github.com/muesli/go-app-paths"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@@ -28,23 +34,34 @@ var (
// CommitSHA as provided by goreleaser.
CommitSHA = ""
- readmeNames = []string{"README.md", "README"}
- configFile string
- pager bool
- style string
- width uint
- showAllFiles bool
- localOnly bool
- mouse bool
+ readmeNames = []string{"README.md", "README", "Readme.md", "Readme", "readme.md", "readme"}
+ configFile string
+ pager bool
+ tui bool
+ style string
+ width uint
+ showAllFiles bool
+ showLineNumbers bool
+ preserveNewLines bool
+ mouse bool
rootCmd = &cobra.Command{
- Use: "glow [SOURCE|DIR]",
- Short: "Render markdown on the CLI, with pizzazz!",
- Long: paragraph(fmt.Sprintf("\nRender markdown on the CLI, %s!", keyword("with pizzazz"))),
+ Use: "glow [SOURCE|DIR]",
+ Short: "Render markdown on the CLI, with pizzazz!",
+ Long: paragraph(
+ fmt.Sprintf("\nRender markdown on the CLI, %s!", keyword("with pizzazz")),
+ ),
SilenceErrors: false,
- SilenceUsage: false,
+ SilenceUsage: true,
TraverseChildren: true,
- RunE: execute,
+ Args: cobra.MaximumNArgs(1),
+ ValidArgsFunction: func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
+ return nil, cobra.ShellCompDirectiveDefault
+ },
+ PersistentPreRunE: func(cmd *cobra.Command, _ []string) error {
+ return validateOptions(cmd)
+ },
+ RunE: execute,
}
)
@@ -62,31 +79,22 @@ func sourceFromArg(arg string) (*source, error) {
}
// a GitHub or GitLab URL (even without the protocol):
- if u, ok := isGitHubURL(arg); ok {
- src, err := findGitHubREADME(u)
- if err != nil {
- return nil, err
- }
- return src, nil
- }
- if u, ok := isGitLabURL(arg); ok {
- src, err := findGitLabREADME(u)
- if err != nil {
- return nil, err
- }
+ src, err := readmeURL(arg)
+ if src != nil && err == nil {
+ // if there's an error, try next methods...
return src, nil
}
// HTTP(S) URLs:
- if u, err := url.ParseRequestURI(arg); err == nil && strings.Contains(arg, "://") {
+ if u, err := url.ParseRequestURI(arg); err == nil && strings.Contains(arg, "://") { //nolint:nestif
if u.Scheme != "" {
if u.Scheme != "http" && u.Scheme != "https" {
return nil, fmt.Errorf("%s is not a supported protocol", u.Scheme)
}
// consumer of the source is responsible for closing the ReadCloser.
- resp, err := http.Get(u.String()) // nolint:bodyclose
+ resp, err := http.Get(u.String()) //nolint: noctx,bodyclose
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("unable to get url: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP status %d", resp.StatusCode)
@@ -101,9 +109,12 @@ func sourceFromArg(arg string) (*source, error) {
arg = "."
}
st, err := os.Stat(arg)
- if err == nil && st.IsDir() {
+ if err == nil && st.IsDir() { //nolint:nestif
var src *source
- _ = filepath.Walk(arg, func(path string, info os.FileInfo, err error) error {
+ _ = filepath.Walk(arg, func(path string, _ os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
for _, v := range readmeNames {
if strings.EqualFold(filepath.Base(path), v) {
r, err := os.Open(path)
@@ -128,28 +139,49 @@ func sourceFromArg(arg string) (*source, error) {
return nil, errors.New("missing markdown source")
}
- // a file:
r, err := os.Open(arg)
- u, _ := filepath.Abs(arg)
- return &source{r, u}, err
+ if err != nil {
+ return nil, fmt.Errorf("unable to open file: %w", err)
+ }
+ u, err := filepath.Abs(arg)
+ if err != nil {
+ return nil, fmt.Errorf("unable to get absolute path: %w", err)
+ }
+ return &source{r, u}, nil
+}
+
+// validateStyle checks if the style is a default style, if not, checks that
+// the custom style exists.
+func validateStyle(style string) error {
+ if style != "auto" && styles.DefaultStyles[style] == nil {
+ style = utils.ExpandPath(style)
+ if _, err := os.Stat(style); errors.Is(err, fs.ErrNotExist) {
+ return fmt.Errorf("specified style does not exist: %s", style)
+ } else if err != nil {
+ return fmt.Errorf("unable to stat file: %w", err)
+ }
+ }
+ return nil
}
func validateOptions(cmd *cobra.Command) error {
// grab config values from Viper
width = viper.GetUint("width")
- localOnly = viper.GetBool("local")
mouse = viper.GetBool("mouse")
pager = viper.GetBool("pager")
+ tui = viper.GetBool("tui")
+ showAllFiles = viper.GetBool("all")
+ preserveNewLines = viper.GetBool("preserveNewLines")
+ showLineNumbers = viper.GetBool("showLineNumbers")
+
+ if pager && tui {
+ return errors.New("cannot use both pager and tui")
+ }
// validate the glamour style
style = viper.GetString("style")
- if style != "auto" && glamour.DefaultStyles[style] == nil {
- style = utils.ExpandPath(style)
- if _, err := os.Stat(style); os.IsNotExist(err) {
- return fmt.Errorf("Specified style does not exist: %s", style)
- } else if err != nil {
- return err
- }
+ if err := validateStyle(style); err != nil {
+ return err
}
isTerminal := term.IsTerminal(int(os.Stdout.Fd()))
@@ -160,18 +192,20 @@ func validateOptions(cmd *cobra.Command) error {
}
// Detect terminal width
- if isTerminal && width == 0 && !cmd.Flags().Changed("width") {
- w, _, err := term.GetSize(int(os.Stdout.Fd()))
- if err == nil {
- width = uint(w)
- }
+ if !cmd.Flags().Changed("width") { //nolint:nestif
+ if isTerminal && width == 0 {
+ w, _, err := term.GetSize(int(os.Stdout.Fd()))
+ if err == nil {
+ width = uint(w) //nolint:gosec
+ }
- if width > 120 {
- width = 120
+ if width > 120 {
+ width = 120
+ }
+ }
+ if width == 0 {
+ width = 80
}
- }
- if width == 0 {
- width = 80
}
return nil
}
@@ -179,7 +213,7 @@ func validateOptions(cmd *cobra.Command) error {
func stdinIsPipe() (bool, error) {
stat, err := os.Stdin.Stat()
if err != nil {
- return false, err
+ return false, fmt.Errorf("unable to open file: %w", err)
}
if stat.Mode()&os.ModeCharDevice == 0 || stat.Size() > 0 {
return true, nil
@@ -188,11 +222,6 @@ func stdinIsPipe() (bool, error) {
}
func execute(cmd *cobra.Command, args []string) error {
- initConfig()
- if err := validateOptions(cmd); err != nil {
- return err
- }
-
// if stdin is a pipe then use stdin for input. note that you can also
// explicitly use a - to read from stdin.
if yes, err := stdinIsPipe(); err != nil {
@@ -206,7 +235,7 @@ func execute(cmd *cobra.Command, args []string) error {
switch len(args) {
// TUI running on cwd
case 0:
- return runTUI("", false)
+ return runTUI("", "")
// TUI with possible dir argument
case 1:
@@ -216,7 +245,7 @@ func execute(cmd *cobra.Command, args []string) error {
if err == nil && info.IsDir() {
p, err := filepath.Abs(args[0])
if err == nil {
- return runTUI(p, false)
+ return runTUI(p, "")
}
}
fallthrough
@@ -246,7 +275,7 @@ func executeArg(cmd *cobra.Command, arg string, w io.Writer) error {
func executeCLI(cmd *cobra.Command, src *source, w io.Writer) error {
b, err := io.ReadAll(src.reader)
if err != nil {
- return err
+ return fmt.Errorf("unable to read from reader: %w", err)
}
b = utils.RemoveFrontmatter(b)
@@ -259,103 +288,106 @@ func executeCLI(cmd *cobra.Command, src *source, w io.Writer) error {
baseURL = u.String() + "/"
}
- // initialize glamour
- var gs glamour.TermRendererOption
- if style == "auto" {
- gs = glamour.WithEnvironmentConfig()
- } else {
- gs = glamour.WithStylePath(style)
- }
+ isCode := !utils.IsMarkdownFile(src.URL)
+ // initialize glamour
r, err := glamour.NewTermRenderer(
- gs,
- glamour.WithWordWrap(int(width)),
+ glamour.WithColorProfile(lipgloss.ColorProfile()),
+ utils.GlamourStyle(style, isCode),
+ glamour.WithWordWrap(int(width)), //nolint:gosec
glamour.WithBaseURL(baseURL),
glamour.WithPreservedNewLines(),
)
if err != nil {
- return err
+ return fmt.Errorf("unable to create renderer: %w", err)
}
- out, err := r.RenderBytes(b)
+ content := string(b)
+ ext := filepath.Ext(src.URL)
+ if isCode {
+ content = utils.WrapCodeBlock(string(b), ext)
+ }
+
+ out, err := r.Render(content)
if err != nil {
- return err
- }
-
- // trim lines
- lines := strings.Split(string(out), "\n")
- var content string
- for i, s := range lines {
- content += strings.TrimSpace(s)
-
- // don't add an artificial newline after the last split
- if i+1 < len(lines) {
- content += "\n"
- }
+ return fmt.Errorf("unable to render markdown: %w", err)
}
// display
- if pager || cmd.Flags().Changed("pager") {
+ switch {
+ case pager || cmd.Flags().Changed("pager"):
pagerCmd := os.Getenv("PAGER")
if pagerCmd == "" {
pagerCmd = "less -r"
}
- pa := strings.Split(pagerCmd, " ")
- c := exec.Command(pa[0], pa[1:]...) // nolint:gosec
- c.Stdin = strings.NewReader(content)
+ fields, err := shell.Fields(pagerCmd, os.Getenv)
+ if err != nil || len(fields) == 0 {
+ return fmt.Errorf("unable to parse PAGER command: %s", pagerCmd)
+ }
+ c := exec.Command(fields[0], fields[1:]...) //nolint:gosec
+ c.Stdin = strings.NewReader(out)
c.Stdout = os.Stdout
- return c.Run()
+ if err := c.Run(); err != nil {
+ return fmt.Errorf("unable to run command: %w", err)
+ }
+ return nil
+ case tui || cmd.Flags().Changed("tui"):
+ path := ""
+ if !isURL(src.URL) {
+ path = src.URL
+ }
+ return runTUI(path, content)
+ default:
+ if _, err = fmt.Fprint(w, out); err != nil {
+ return fmt.Errorf("unable to write to writer: %w", err)
+ }
+ return nil
}
-
- fmt.Fprint(w, content)
- return nil
}
-func runTUI(workingDirectory string, stashedOnly bool) error {
+func runTUI(path string, content string) error {
// Read environment to get debugging stuff
- var cfg ui.Config
- if err := babyenv.Parse(&cfg); err != nil {
+ cfg, err := env.ParseAs[ui.Config]()
+ if err != nil {
return fmt.Errorf("error parsing config: %v", err)
}
- // Log to file, if set
- if cfg.Logfile != "" {
- f, err := tea.LogToFile(cfg.Logfile, "glow")
- if err != nil {
- return err
- }
- defer f.Close() //nolint:errcheck
+ // use style set in env, or auto if unset
+ if err := validateStyle(cfg.GlamourStyle); err != nil {
+ cfg.GlamourStyle = style
}
- cfg.WorkingDirectory = workingDirectory
- cfg.DocumentTypes = ui.NewDocTypeSet()
+ cfg.Path = path
cfg.ShowAllFiles = showAllFiles
+ cfg.ShowLineNumbers = showLineNumbers
cfg.GlamourMaxWidth = width
- cfg.GlamourStyle = style
cfg.EnableMouse = mouse
-
- if stashedOnly {
- cfg.DocumentTypes.Add(ui.StashedDoc, ui.NewsDoc)
- } else if localOnly {
- cfg.DocumentTypes.Add(ui.LocalDoc)
- }
+ cfg.PreserveNewLines = preserveNewLines
// Run Bubble Tea program
- if _, err := ui.NewProgram(cfg).Run(); err != nil {
- return err
+ if _, err := ui.NewProgram(cfg, content).Run(); err != nil {
+ return fmt.Errorf("unable to run tui program: %w", err)
}
return nil
}
func main() {
- if err := rootCmd.Execute(); err != nil {
+ closer, err := setupLog()
+ if err != nil {
+ fmt.Println(err)
os.Exit(1)
}
+ if err := rootCmd.Execute(); err != nil {
+ _ = closer()
+ os.Exit(1)
+ }
+ _ = closer()
}
func init() {
+ tryLoadConfigFromDefaultPlaces()
if len(CommitSHA) >= 7 {
vt := rootCmd.VersionTemplate()
rootCmd.SetVersionTemplate(vt[:len(vt)-1] + " (" + CommitSHA[0:7] + ")\n")
@@ -364,63 +396,78 @@ func init() {
Version = "unknown (built from source)"
}
rootCmd.Version = Version
-
- scope := gap.NewScope(gap.User, "glow")
- defaultConfigFile, _ := scope.ConfigPath("glow.yml")
+ rootCmd.InitDefaultCompletionCmd()
// "Glow Classic" cli arguments
- rootCmd.PersistentFlags().StringVar(&configFile, "config", "", fmt.Sprintf("config file (default %s)", defaultConfigFile))
+ rootCmd.PersistentFlags().StringVar(&configFile, "config", "", fmt.Sprintf("config file (default %s)", viper.GetViper().ConfigFileUsed()))
rootCmd.Flags().BoolVarP(&pager, "pager", "p", false, "display with pager")
- rootCmd.Flags().StringVarP(&style, "style", "s", "auto", "style name or JSON path")
- rootCmd.Flags().UintVarP(&width, "width", "w", 0, "word-wrap at width")
+ rootCmd.Flags().BoolVarP(&tui, "tui", "t", false, "display with tui")
+ rootCmd.Flags().StringVarP(&style, "style", "s", styles.AutoStyle, "style name or JSON path")
+ rootCmd.Flags().UintVarP(&width, "width", "w", 0, "word-wrap at width (set to 0 to disable)")
rootCmd.Flags().BoolVarP(&showAllFiles, "all", "a", false, "show system files and directories (TUI-mode only)")
- rootCmd.Flags().BoolVarP(&localOnly, "local", "l", false, "show local files only; no network (TUI-mode only)")
+ rootCmd.Flags().BoolVarP(&showLineNumbers, "line-numbers", "l", false, "show line numbers (TUI-mode only)")
+ rootCmd.Flags().BoolVarP(&preserveNewLines, "preserve-new-lines", "n", false, "preserve newlines in the output")
rootCmd.Flags().BoolVarP(&mouse, "mouse", "m", false, "enable mouse wheel (TUI-mode only)")
_ = rootCmd.Flags().MarkHidden("mouse")
// Config bindings
+ _ = viper.BindPFlag("pager", rootCmd.Flags().Lookup("pager"))
+ _ = viper.BindPFlag("tui", rootCmd.Flags().Lookup("tui"))
_ = viper.BindPFlag("style", rootCmd.Flags().Lookup("style"))
_ = viper.BindPFlag("width", rootCmd.Flags().Lookup("width"))
- _ = viper.BindPFlag("local", rootCmd.Flags().Lookup("local"))
+ _ = viper.BindPFlag("debug", rootCmd.Flags().Lookup("debug"))
_ = viper.BindPFlag("mouse", rootCmd.Flags().Lookup("mouse"))
- viper.SetDefault("style", "auto")
+ _ = viper.BindPFlag("preserveNewLines", rootCmd.Flags().Lookup("preserve-new-lines"))
+ _ = viper.BindPFlag("showLineNumbers", rootCmd.Flags().Lookup("line-numbers"))
+ _ = viper.BindPFlag("all", rootCmd.Flags().Lookup("all"))
+
+ viper.SetDefault("style", styles.AutoStyle)
viper.SetDefault("width", 0)
- viper.SetDefault("local", "false")
+ viper.SetDefault("all", true)
- // Stash
- stashCmd.PersistentFlags().StringVarP(&memo, "memo", "m", "", "memo/note for stashing")
- rootCmd.AddCommand(stashCmd)
-
- rootCmd.AddCommand(configCmd)
+ rootCmd.AddCommand(configCmd, manCmd)
}
-func initConfig() {
- if configFile != "" {
- viper.SetConfigFile(configFile)
- } else {
- scope := gap.NewScope(gap.User, "glow")
- dirs, err := scope.ConfigDirs()
- if err != nil {
- fmt.Println("Can't retrieve default config. Please manually pass a config file with '--config'")
- os.Exit(1)
- }
-
- for _, v := range dirs {
- viper.AddConfigPath(v)
- }
- viper.SetConfigName("glow")
- viper.SetConfigType("yaml")
+func tryLoadConfigFromDefaultPlaces() {
+ scope := gap.NewScope(gap.User, "glow")
+ dirs, err := scope.ConfigDirs()
+ if err != nil {
+ fmt.Println("Could not load find configuration directory.")
+ os.Exit(1)
}
+ if c := os.Getenv("XDG_CONFIG_HOME"); c != "" {
+ dirs = append([]string{filepath.Join(c, "glow")}, dirs...)
+ }
+
+ if c := os.Getenv("GLOW_CONFIG_HOME"); c != "" {
+ dirs = append([]string{c}, dirs...)
+ }
+
+ for _, v := range dirs {
+ viper.AddConfigPath(v)
+ }
+
+ viper.SetConfigName("glow")
+ viper.SetConfigType("yaml")
viper.SetEnvPrefix("glow")
viper.AutomaticEnv()
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
- fmt.Println("Error parsing config:", err)
- os.Exit(1)
+ log.Warn("Could not parse configuration file", "err", err)
}
}
- // fmt.Println("Using config file:", viper.ConfigFileUsed())
+ if used := viper.ConfigFileUsed(); used != "" {
+ log.Debug("Using configuration file", "path", viper.ConfigFileUsed())
+ return
+ }
+
+ if viper.ConfigFileUsed() == "" {
+ configFile = filepath.Join(dirs[0], "glow.yml")
+ }
+ if err := ensureConfigFile(); err != nil {
+ log.Error("Could not create default configuration", "error", err)
+ }
}
diff --git a/man_cmd.go b/man_cmd.go
new file mode 100644
index 0000000..a179ef7
--- /dev/null
+++ b/man_cmd.go
@@ -0,0 +1,29 @@
+package main
+
+import (
+ "fmt"
+ "os"
+
+ mcobra "github.com/muesli/mango-cobra"
+ "github.com/muesli/roff"
+ "github.com/spf13/cobra"
+)
+
+var manCmd = &cobra.Command{
+ Use: "man",
+ Short: "Generates manpages",
+ SilenceUsage: true,
+ DisableFlagsInUseLine: true,
+ Hidden: true,
+ Args: cobra.NoArgs,
+ RunE: func(*cobra.Command, []string) error {
+ manPage, err := mcobra.NewManPage(1, rootCmd)
+ if err != nil {
+ return fmt.Errorf("unable to instantiate man page: %w", err)
+ }
+ if _, err := fmt.Fprint(os.Stdout, manPage.Build(roff.NewDocument())); err != nil {
+ return fmt.Errorf("unable to build man page: %w", err)
+ }
+ return nil
+ },
+}
diff --git a/stash_cmd.go b/stash_cmd.go
deleted file mode 100644
index efb8bbd..0000000
--- a/stash_cmd.go
+++ /dev/null
@@ -1,82 +0,0 @@
-package main
-
-import (
- "fmt"
- "io"
- "log"
- "os"
- "path"
- "strings"
-
- "github.com/charmbracelet/charm"
- "github.com/charmbracelet/lipgloss"
- "github.com/spf13/cobra"
-)
-
-var (
- memo string
- dot = lipgloss.NewStyle().Foreground(lipgloss.Color("#04B575")).Render("•")
-
- stashCmd = &cobra.Command{
- Use: "stash [SOURCE]",
- Hidden: false,
- Short: "Stash a markdown",
- Long: paragraph(fmt.Sprintf("\nDo %s stuff. Run with no arguments to browse your stash or pass a path to a markdown file to stash it.", keyword("stash"))),
- Example: paragraph("glow stash\nglow stash README.md\nglow stash -m \"secret notes\" path/to/notes.md"),
- Args: cobra.MaximumNArgs(1),
- RunE: func(cmd *cobra.Command, args []string) error {
- initConfig()
- if len(args) == 0 {
- return runTUI("", true)
- }
-
- filePath := args[0]
-
- if memo == "" {
- memo = strings.Replace(path.Base(filePath), path.Ext(filePath), "", 1)
- }
-
- cc := initCharmClient()
- f, err := os.Open(filePath)
- if err != nil {
- return fmt.Errorf("bad filename")
- }
-
- defer f.Close() //nolint:errcheck
- b, err := io.ReadAll(f)
- if err != nil {
- return fmt.Errorf("error reading file")
- }
-
- _, err = cc.StashMarkdown(memo, string(b))
- if err != nil {
- return fmt.Errorf("error stashing markdown")
- }
-
- fmt.Println(dot + " Stashed!")
- return nil
- },
- }
-)
-
-func getCharmConfig() *charm.Config {
- cfg, err := charm.ConfigFromEnv()
- if err != nil {
- log.Fatal(err)
- }
-
- return cfg
-}
-
-func initCharmClient() *charm.Client {
- cfg := getCharmConfig()
- cc, err := charm.NewClient(cfg)
- if err == charm.ErrMissingSSHAuth {
- fmt.Println(paragraph("We had some trouble authenticating via SSH. If this continues to happen the Charm tool may be able to help you. More info at https://github.com/charmbracelet/charm."))
- os.Exit(1)
- } else if err != nil {
- fmt.Println(err)
- os.Exit(1)
- }
- return cc
-}
diff --git a/style.go b/style.go
index 366bbc4..b688a4e 100644
--- a/style.go
+++ b/style.go
@@ -1,13 +1,13 @@
package main
-import . "github.com/charmbracelet/lipgloss" //nolint:revive
+import "github.com/charmbracelet/lipgloss"
var (
- keyword = NewStyle().
- Foreground(AdaptiveColor{Light: "#04B575", Dark: "#04B575"}).
+ keyword = lipgloss.NewStyle().
+ Foreground(lipgloss.Color("#04B575")).
Render
- paragraph = NewStyle().
+ paragraph = lipgloss.NewStyle().
Width(78).
Padding(0, 0, 0, 2).
Render
diff --git a/ui/config.go b/ui/config.go
index 9db2340..001d5b8 100644
--- a/ui/config.go
+++ b/ui/config.go
@@ -2,29 +2,19 @@ package ui
// Config contains TUI-specific configuration.
type Config struct {
- ShowAllFiles bool
- Gopath string `env:"GOPATH"`
- HomeDir string `env:"HOME"`
- GlamourMaxWidth uint
- GlamourStyle string
- EnableMouse bool
+ ShowAllFiles bool
+ ShowLineNumbers bool
+ Gopath string `env:"GOPATH"`
+ HomeDir string `env:"HOME"`
+ GlamourMaxWidth uint
+ GlamourStyle string `env:"GLAMOUR_STYLE"`
+ EnableMouse bool
+ PreserveNewLines bool
- // Which directory should we start from?
- WorkingDirectory string
-
- // Which document types shall we show?
- DocumentTypes DocTypeSet
+ // Working directory or file path
+ Path string
// For debugging the UI
- Logfile string `env:"GLOW_LOGFILE"`
- HighPerformancePager bool `env:"GLOW_HIGH_PERFORMANCE_PAGER" default:"true"`
- GlamourEnabled bool `env:"GLOW_ENABLE_GLAMOUR" default:"true"`
-}
-
-func (c Config) localOnly() bool {
- return c.DocumentTypes.Equals(NewDocTypeSet(LocalDoc))
-}
-
-func (c Config) stashedOnly() bool {
- return c.DocumentTypes.Contains(StashedDoc) && !c.DocumentTypes.Contains(LocalDoc)
+ HighPerformancePager bool `env:"GLOW_HIGH_PERFORMANCE_PAGER" envDefault:"true"`
+ GlamourEnabled bool `env:"GLOW_ENABLE_GLAMOUR" envDefault:"true"`
}
diff --git a/ui/consts_unix.go b/ui/consts_unix.go
deleted file mode 100644
index 57f75dd..0000000
--- a/ui/consts_unix.go
+++ /dev/null
@@ -1,8 +0,0 @@
-//go:build !windows
-// +build !windows
-
-package ui
-
-const (
- pagerStashIcon = "🔒"
-)
diff --git a/ui/consts_windows.go b/ui/consts_windows.go
deleted file mode 100644
index 25f9851..0000000
--- a/ui/consts_windows.go
+++ /dev/null
@@ -1,7 +0,0 @@
-// +build windows
-
-package ui
-
-const (
- pagerStashIcon = "•"
-)
diff --git a/ui/doctypes.go b/ui/doctypes.go
deleted file mode 100644
index 8d2ef14..0000000
--- a/ui/doctypes.go
+++ /dev/null
@@ -1,90 +0,0 @@
-package ui
-
-// DocType represents a type of markdown document.
-type DocType int
-
-// Available document types.
-const (
- NoDocType DocType = iota
- LocalDoc
- StashedDoc
- ConvertedDoc
- NewsDoc
-)
-
-func (d DocType) String() string {
- return [...]string{
- "none",
- "local",
- "stashed",
- "converted",
- "news",
- }[d]
-}
-
-// DocTypeSet is a set (in the mathematic sense) of document types.
-type DocTypeSet map[DocType]struct{}
-
-// NewDocTypeSet returns a set of document types.
-func NewDocTypeSet(t ...DocType) DocTypeSet {
- d := DocTypeSet(make(map[DocType]struct{}))
- if len(t) > 0 {
- d.Add(t...)
- }
- return d
-}
-
-// Add adds a document type of the set.
-func (d *DocTypeSet) Add(t ...DocType) int {
- for _, v := range t {
- (*d)[v] = struct{}{}
- }
- return len(*d)
-}
-
-// Contains returns whether or not the set contains the given DocTypes.
-func (d DocTypeSet) Contains(m ...DocType) bool {
- matches := 0
- for _, t := range m {
- if _, found := d[t]; found {
- matches++
- }
- }
- return matches == len(m)
-}
-
-// Difference return a DocumentType set that does not contain the given types.
-func (d DocTypeSet) Difference(t ...DocType) DocTypeSet {
- c := copyDocumentTypes(d)
- for k := range c {
- for _, docType := range t {
- if k == docType {
- delete(c, k)
- break
- }
- }
- }
- return c
-}
-
-// Equals returns whether or not the two sets are equal.
-func (d DocTypeSet) Equals(other DocTypeSet) bool {
- return d.Contains(other.AsSlice()...) && len(d) == len(other)
-}
-
-// AsSlice returns the set as a slice of document types.
-func (d DocTypeSet) AsSlice() (agg []DocType) {
- for k := range d {
- agg = append(agg, k)
- }
- return
-}
-
-// Return a copy of the given DoctTypes map.
-func copyDocumentTypes(d DocTypeSet) DocTypeSet {
- c := make(map[DocType]struct{})
- for k, v := range d {
- c[k] = v
- }
- return c
-}
diff --git a/ui/doctypes_test.go b/ui/doctypes_test.go
deleted file mode 100644
index e882b14..0000000
--- a/ui/doctypes_test.go
+++ /dev/null
@@ -1,48 +0,0 @@
-package ui
-
-import (
- "reflect"
- "testing"
-)
-
-func TestDocTypeContains(t *testing.T) {
- d := NewDocTypeSet(LocalDoc)
-
- if !d.Contains(LocalDoc) {
- t.Error("Contains reported it doesn't contain a value which it absolutely does contain")
- }
-
- if d.Contains(NewsDoc) {
- t.Error("Contains reported the set contains a value it certainly does not")
- }
-}
-
-func TestDocTypeDifference(t *testing.T) {
- original := NewDocTypeSet(LocalDoc, StashedDoc, ConvertedDoc, NewsDoc)
- difference := original.Difference(LocalDoc, NewsDoc)
- expected := NewDocTypeSet(StashedDoc, ConvertedDoc)
-
- // Make sure the difference operation worked
- if !reflect.DeepEqual(difference, expected) {
- t.Errorf("difference returned %+v; expected %+v", difference, expected)
- }
-
- // Make sure original set was not mutated
- if reflect.DeepEqual(original, difference) {
- t.Errorf("original set was mutated when it should not have been")
- }
-}
-
-func TestDocTypeEquality(t *testing.T) {
- a := NewDocTypeSet(LocalDoc, StashedDoc)
- b := NewDocTypeSet(LocalDoc, StashedDoc)
- c := NewDocTypeSet(LocalDoc)
-
- if !a.Equals(b) {
- t.Errorf("Equality test failed for %+v and %+v; expected true, got false", a, b)
- }
-
- if a.Equals(c) {
- t.Errorf("Equality test failed for %+v and %+v; expected false, got true", a, c)
- }
-}
diff --git a/ui/editor.go b/ui/editor.go
index 60cc320..a567c59 100644
--- a/ui/editor.go
+++ b/ui/editor.go
@@ -1,33 +1,19 @@
package ui
import (
- "os"
- "os/exec"
- "strings"
-
tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/x/editor"
)
-const defaultEditor = "nano"
-
type editorFinishedMsg struct{ err error }
-func openEditor(path string) tea.Cmd {
- editor, args := getEditor()
- cmd := exec.Command(editor, append(args, path)...)
+func openEditor(path string, lineno int) tea.Cmd {
cb := func(err error) tea.Msg {
return editorFinishedMsg{err}
}
+ cmd, err := editor.Cmd("Glow", path, editor.LineNumber(uint(lineno))) //nolint:gosec
+ if err != nil {
+ return func() tea.Msg { return cb(err) }
+ }
return tea.ExecProcess(cmd, cb)
}
-
-func getEditor() (string, []string) {
- editor := strings.Fields(os.Getenv("EDITOR"))
- if len(editor) > 1 {
- return editor[0], editor[1:]
- }
- if len(editor) == 1 {
- return editor[0], []string{}
- }
- return defaultEditor, []string{}
-}
diff --git a/ui/ignore_darwin.go b/ui/ignore_darwin.go
index cccd305..b69437a 100644
--- a/ui/ignore_darwin.go
+++ b/ui/ignore_darwin.go
@@ -1,13 +1,14 @@
+//go:build darwin
// +build darwin
package ui
import "path/filepath"
-func ignorePatterns(m model) []string {
+func ignorePatterns(m commonModel) []string {
return []string{
- filepath.Join(m.common.cfg.HomeDir, "Library"),
- m.common.cfg.Gopath,
+ filepath.Join(m.cfg.HomeDir, "Library"),
+ m.cfg.Gopath,
"node_modules",
".*",
}
diff --git a/ui/ignore_general.go b/ui/ignore_general.go
index 43c1dd1..f4dc8be 100644
--- a/ui/ignore_general.go
+++ b/ui/ignore_general.go
@@ -3,9 +3,9 @@
package ui
-func ignorePatterns(m model) []string {
+func ignorePatterns(m commonModel) []string {
return []string{
- m.common.cfg.Gopath,
+ m.cfg.Gopath,
"node_modules",
".*",
}
diff --git a/ui/markdown.go b/ui/markdown.go
index c5fbff4..c67b1dc 100644
--- a/ui/markdown.go
+++ b/ui/markdown.go
@@ -1,42 +1,19 @@
package ui
import (
- "log"
+ "fmt"
"math"
- "path"
- "strings"
"time"
"unicode"
- "github.com/charmbracelet/charm"
+ "github.com/charmbracelet/log"
"github.com/dustin/go-humanize"
- "github.com/segmentio/ksuid"
"golang.org/x/text/runes"
"golang.org/x/text/transform"
"golang.org/x/text/unicode/norm"
)
-// markdown wraps charm.Markdown.
type markdown struct {
- docType DocType
-
- // Stash identifier. This exists so we can keep track of documents stashed
- // in-session as they relate to their original, non-stashed counterparts.
- // All documents have a stashID, however when a document is stashed that
- // document inherits the stashID of the original.
- stashID ksuid.KSUID
-
- // Unique identifier. Unlike the stash identifier, this value should always
- // be unique so we can confidently find it an operate on it (versus stashID,
- // which could match both an original or stashed document).
- uniqueID ksuid.KSUID
-
- // Some of the document's original values before this document was stashed.
- // These are irrelevant if this document was not stashed in this session.
- originalDocType DocType
- originalTimestamp time.Time
- originalNote string
-
// Full path of a local markdown file. Only relevant to local documents and
// those that have been stashed in this session.
localPath string
@@ -46,102 +23,24 @@ type markdown struct {
// field is ephemeral, and should only be referenced during filtering.
filterValue string
- charm.Markdown
-}
-
-func (m *markdown) generateIDs() {
- if m.stashID.IsNil() {
- m.stashID = ksuid.New()
- }
- m.uniqueID = ksuid.New()
-}
-
-// convertToStashed converts this document into its stashed state.
-func (m *markdown) convertToStashed() {
- if m.docType == ConvertedDoc {
- if debug {
- log.Println("not converting already converted document:", m)
- }
- return
- }
-
- m.originalDocType = m.docType
- m.originalTimestamp = m.CreatedAt
- m.originalNote = m.Note
-
- if m.docType == LocalDoc {
- m.Note = strings.Replace(path.Base(m.localPath), path.Ext(m.localPath), "", 1)
- }
- m.CreatedAt = time.Now()
- m.docType = ConvertedDoc
-}
-
-// revert reverts this document from its stashed state.
-func (m *markdown) revertFromStashed() {
- if m.docType != ConvertedDoc {
- log.Printf("not reverting document of type %s: %v", m.docType, m)
- }
-
- m.docType = m.originalDocType
- m.CreatedAt = m.originalTimestamp
- m.Note = m.originalNote
+ Body string
+ Note string
+ Modtime time.Time
}
// Generate the value we're doing to filter against.
func (m *markdown) buildFilterValue() {
note, err := normalize(m.Note)
if err != nil {
- if debug {
- log.Printf("error normalizing '%s': %v", m.Note, err)
- }
+ log.Error("error normalizing", "note", m.Note, "error", err)
m.filterValue = m.Note
}
m.filterValue = note
}
-// shouldSortAsLocal returns whether or not this markdown should be sorted as though
-// it's a local markdown document.
-func (m markdown) shouldSortAsLocal() bool {
- return m.docType == LocalDoc || m.docType == ConvertedDoc
-}
-
-// Sort documents with local files first, then by date.
-type markdownsByLocalFirst []*markdown
-
-func (m markdownsByLocalFirst) Len() int { return len(m) }
-func (m markdownsByLocalFirst) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
-func (m markdownsByLocalFirst) Less(i, j int) bool {
- iIsLocal := m[i].shouldSortAsLocal()
- jIsLocal := m[j].shouldSortAsLocal()
-
- // Local files (and files that used to be local) come first
- if iIsLocal && !jIsLocal {
- return true
- }
- if !iIsLocal && jIsLocal {
- return false
- }
-
- // If both are local files, sort by filename. Note that we should never
- // hit equality here since two files can't have the same path.
- if iIsLocal && jIsLocal {
- return strings.Compare(m[i].localPath, m[j].localPath) == -1
- }
-
- // Neither are local files so sort by date descending
- if !m[i].CreatedAt.Equal(m[j].CreatedAt) {
- return m[i].CreatedAt.After(m[j].CreatedAt)
- }
-
- // If the times also match, sort by unqiue ID.
- ids := []ksuid.KSUID{m[i].uniqueID, m[j].uniqueID}
- ksuid.Sort(ids)
- return ids[0] == m[i].uniqueID
-}
-
func (m markdown) relativeTime() string {
- return relativeTime(m.CreatedAt)
+ return relativeTime(m.Modtime)
}
// Normalize text to aid in the filtering process. In particular, we remove
@@ -150,19 +49,10 @@ func (m markdown) relativeTime() string {
func normalize(in string) (string, error) {
t := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
out, _, err := transform.String(t, in)
- return out, err
-}
-
-// wrapMarkdowns wraps a *charm.Markdown with a *markdown in order to add some
-// extra metadata.
-func wrapMarkdowns(t DocType, md []*charm.Markdown) (m []*markdown) {
- for _, v := range md {
- m = append(m, &markdown{
- docType: t,
- Markdown: *v,
- })
+ if err != nil {
+ return "", fmt.Errorf("error normalizing: %w", err)
}
- return m
+ return out, nil
}
// Return the time in a human-readable format relative to the current time.
diff --git a/ui/pager.go b/ui/pager.go
index 8196761..5c522e7 100644
--- a/ui/pager.go
+++ b/ui/pager.go
@@ -2,25 +2,29 @@ package ui
import (
"fmt"
- "log"
"math"
+ "path/filepath"
"strings"
"time"
"github.com/atotto/clipboard"
- "github.com/charmbracelet/bubbles/spinner"
- "github.com/charmbracelet/bubbles/textinput"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
- "github.com/charmbracelet/charm"
"github.com/charmbracelet/glamour"
+ "github.com/charmbracelet/glow/v2/utils"
"github.com/charmbracelet/lipgloss"
+ "github.com/charmbracelet/log"
+ "github.com/fsnotify/fsnotify"
runewidth "github.com/mattn/go-runewidth"
"github.com/muesli/reflow/ansi"
"github.com/muesli/reflow/truncate"
+ "github.com/muesli/termenv"
)
-const statusBarHeight = 1
+const (
+ statusBarHeight = 1
+ lineNumberWidth = 4
+)
var (
pagerHelpHeight int
@@ -28,11 +32,7 @@ var (
mintGreen = lipgloss.AdaptiveColor{Light: "#89F0CB", Dark: "#89F0CB"}
darkGreen = lipgloss.AdaptiveColor{Light: "#1C8760", Dark: "#1C8760"}
- noteHeading = lipgloss.NewStyle().
- Foreground(cream).
- Background(green).
- Padding(0, 1).
- Render("Set Memo")
+ lineNumberFg = lipgloss.AdaptiveColor{Light: "#656565", Dark: "#7D7D7D"}
statusBarNoteFg = lipgloss.AdaptiveColor{Light: "#656565", Dark: "#7D7D7D"}
statusBarBg = lipgloss.AdaptiveColor{Light: "#E6E6E6", Dark: "#242424"}
@@ -52,21 +52,11 @@ var (
Background(lipgloss.AdaptiveColor{Light: "#DCDCDC", Dark: "#323232"}).
Render
- statusBarStashDotStyle = lipgloss.NewStyle().
- Foreground(green).
- Background(statusBarBg).
- Render
-
statusBarMessageStyle = lipgloss.NewStyle().
Foreground(mintGreen).
Background(darkGreen).
Render
- statusBarMessageStashIconStyle = lipgloss.NewStyle().
- Foreground(mintGreen).
- Background(darkGreen).
- Render
-
statusBarMessageScrollPosStyle = lipgloss.NewStyle().
Foreground(mintGreen).
Background(darkGreen).
@@ -82,47 +72,28 @@ var (
Background(lipgloss.AdaptiveColor{Light: "#f2f2f2", Dark: "#1B1B1B"}).
Render
- spinnerStyle = lipgloss.NewStyle().
- Foreground(statusBarNoteFg).
- Background(statusBarBg)
-
- pagerNoteInputPromptStyle = lipgloss.NewStyle().
- Foreground(darkGray).
- Background(yellowGreen).
- Padding(0, 1)
-
- pagerNoteInputStyle = lipgloss.NewStyle().
- Foreground(darkGray).
- Background(yellowGreen)
-
- pagerNoteInputCursorStyle = lipgloss.NewStyle().
- Foreground(fuschia)
+ lineNumberStyle = lipgloss.NewStyle().
+ Foreground(lineNumberFg).
+ Render
)
type (
contentRenderedMsg string
- noteSavedMsg *charm.Markdown
+ reloadMsg struct{}
)
type pagerState int
const (
pagerStateBrowse pagerState = iota
- pagerStateSetNote
- pagerStateStashing
- pagerStateStashSuccess
pagerStateStatusMessage
)
type pagerModel struct {
- common *commonModel
- viewport viewport.Model
- state pagerState
- showHelp bool
- textInput textinput.Model
-
- spinner spinner.Model
- spinnerStart time.Time
+ common *commonModel
+ viewport viewport.Model
+ state pagerState
+ showHelp bool
statusMessage string
statusMessageTimer *time.Timer
@@ -131,9 +102,7 @@ type pagerModel struct {
// it here so we can re-render it on resize.
currentDocument markdown
- // Newly stashed markdown. We store it here temporarily so we can replace
- // currentDocument above after a stash.
- stashedDocument *markdown
+ watcher *fsnotify.Watcher
}
func newPagerModel(common *commonModel) pagerModel {
@@ -142,34 +111,18 @@ func newPagerModel(common *commonModel) pagerModel {
vp.YPosition = 0
vp.HighPerformanceRendering = config.HighPerformancePager
- // Text input for notes/memos
- ti := textinput.New()
- ti.Prompt = " > "
- ti.PromptStyle = pagerNoteInputPromptStyle
- ti.TextStyle = pagerNoteInputStyle
- ti.CursorStyle = pagerNoteInputCursorStyle
- ti.CharLimit = noteCharacterLimit
- ti.Focus()
-
- // Text input for search
- sp := spinner.New()
- sp.Style = spinnerStyle
-
- return pagerModel{
- common: common,
- state: pagerStateBrowse,
- textInput: ti,
- viewport: vp,
- spinner: sp,
+ m := pagerModel{
+ common: common,
+ state: pagerStateBrowse,
+ viewport: vp,
}
+ m.initWatcher()
+ return m
}
func (m *pagerModel) setSize(w, h int) {
m.viewport.Width = w
m.viewport.Height = h - statusBarHeight
- m.textInput.Width = w -
- ansi.PrintableRuneWidth(noteHeading) -
- ansi.PrintableRuneWidth(m.textInput.Prompt) - 1
if m.showHelp {
if pagerHelpHeight == 0 {
@@ -191,13 +144,18 @@ func (m *pagerModel) toggleHelp() {
}
}
+type pagerStatusMessage struct {
+ message string
+ isError bool
+}
+
// Perform stuff that needs to happen after a successful markdown stash. Note
-// that the the returned command should be sent back the through the pager
+// that the returned command should be sent back the through the pager
// update function.
-func (m *pagerModel) showStatusMessage(statusMessage string) tea.Cmd {
+func (m *pagerModel) showStatusMessage(msg pagerStatusMessage) tea.Cmd {
// Show a success message to the user
m.state = pagerStateStatusMessage
- m.statusMessage = statusMessage
+ m.statusMessage = msg.message
if m.statusMessageTimer != nil {
m.statusMessageTimer.Stop()
}
@@ -207,6 +165,7 @@ func (m *pagerModel) showStatusMessage(statusMessage string) tea.Cmd {
}
func (m *pagerModel) unload() {
+ log.Debug("unload")
if m.showHelp {
m.toggleHelp()
}
@@ -216,7 +175,7 @@ func (m *pagerModel) unload() {
m.state = pagerStateBrowse
m.viewport.SetContent("")
m.viewport.YOffset = 0
- m.textInput.Reset()
+ m.unwatchFile()
}
func (m pagerModel) update(msg tea.Msg) (pagerModel, tea.Cmd) {
@@ -227,134 +186,77 @@ func (m pagerModel) update(msg tea.Msg) (pagerModel, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
- switch m.state {
- case pagerStateSetNote:
- switch msg.String() {
- case keyEsc:
+ switch msg.String() {
+ case "q", keyEsc:
+ if m.state != pagerStateBrowse {
m.state = pagerStateBrowse
return m, nil
- case keyEnter:
- var cmd tea.Cmd
- if m.textInput.Value() != m.currentDocument.Note { // don't update if the note didn't change
- m.currentDocument.Note = m.textInput.Value() // update optimistically
- cmd = saveDocumentNote(m.common.cc, m.currentDocument.ID, m.currentDocument.Note)
- }
- m.state = pagerStateBrowse
- m.textInput.Reset()
- return m, cmd
}
- default:
- switch msg.String() {
- case "q", keyEsc:
- if m.state != pagerStateBrowse {
- m.state = pagerStateBrowse
- return m, nil
- }
- case "home", "g":
- m.viewport.GotoTop()
- if m.viewport.HighPerformanceRendering {
- cmds = append(cmds, viewport.Sync(m.viewport))
- }
- case "end", "G":
- m.viewport.GotoBottom()
- if m.viewport.HighPerformanceRendering {
- cmds = append(cmds, viewport.Sync(m.viewport))
- }
- case "m":
- isStashed := m.currentDocument.docType == StashedDoc ||
- m.currentDocument.docType == ConvertedDoc
-
- // Users can only set the note on user-stashed markdown
- if !isStashed {
- break
- }
-
- m.state = pagerStateSetNote
-
- // Stop the timer for hiding a status message since changing
- // the state above will have cleared it.
- if m.statusMessageTimer != nil {
- m.statusMessageTimer.Stop()
- }
-
- // Pre-populate note with existing value
- if m.textInput.Value() == "" {
- m.textInput.SetValue(m.currentDocument.Note)
- m.textInput.CursorEnd()
- }
-
- return m, textinput.Blink
-
- case "e":
- if m.currentDocument.docType == LocalDoc {
- return m, openEditor(m.currentDocument.localPath)
- }
-
- case "c":
- err := clipboard.WriteAll(m.currentDocument.Body)
- if err != nil {
- cmds = append(cmds, m.showStatusMessage("Unable to copy contents"))
- } else {
- cmds = append(cmds, m.showStatusMessage("Copied contents"))
- }
-
- case "s":
- if m.common.authStatus != authOK {
- break
- }
-
- md := m.currentDocument
-
- _, alreadyStashed := m.common.filesStashed[md.stashID]
- if alreadyStashed {
- cmds = append(cmds, m.showStatusMessage("Already stashed"))
- break
- }
-
- // Stash a local document
- if m.state != pagerStateStashing && stashableDocTypes.Contains(md.docType) {
- m.state = pagerStateStashing
- m.spinnerStart = time.Now()
- cmds = append(
- cmds,
- stashDocument(m.common.cc, md),
- m.spinner.Tick,
- )
- }
- case "?":
- m.toggleHelp()
- if m.viewport.HighPerformanceRendering {
- cmds = append(cmds, viewport.Sync(m.viewport))
- }
+ case "home", "g":
+ m.viewport.GotoTop()
+ if m.viewport.HighPerformanceRendering {
+ cmds = append(cmds, viewport.Sync(m.viewport))
+ }
+ case "end", "G":
+ m.viewport.GotoBottom()
+ if m.viewport.HighPerformanceRendering {
+ cmds = append(cmds, viewport.Sync(m.viewport))
}
- }
- case spinner.TickMsg:
- spinnerMinTimeout := m.spinnerStart.
- Add(spinnerVisibilityTimeout).
- Add(spinnerMinLifetime)
+ case "d":
+ m.viewport.HalfViewDown()
+ if m.viewport.HighPerformanceRendering {
+ cmds = append(cmds, viewport.Sync(m.viewport))
+ }
- if m.state == pagerStateStashing || time.Now().Before(spinnerMinTimeout) {
- // We're either still stashing or we haven't reached the spinner's
- // full lifetime. In either case we need to spin the spinner
- // irrespective of it's more fine-grained visibility rules.
- var cmd tea.Cmd
- m.spinner, cmd = m.spinner.Update(msg)
- cmds = append(cmds, cmd)
- } else if m.state == pagerStateStashSuccess {
- // Successful stash. Stop spinning and update accordingly.
- m.state = pagerStateBrowse
- m.currentDocument = *m.stashedDocument
- m.stashedDocument = nil
- cmds = append(cmds, m.showStatusMessage("Stashed!"))
+ case "u":
+ m.viewport.HalfViewUp()
+ if m.viewport.HighPerformanceRendering {
+ cmds = append(cmds, viewport.Sync(m.viewport))
+ }
+
+ case "e":
+ lineno := int(math.RoundToEven(float64(m.viewport.TotalLineCount()) * m.viewport.ScrollPercent()))
+ if m.viewport.AtTop() {
+ lineno = 0
+ }
+ log.Info(
+ "opening editor",
+ "file", m.currentDocument.localPath,
+ "line", fmt.Sprintf("%d/%d", lineno, m.viewport.TotalLineCount()),
+ )
+ return m, openEditor(m.currentDocument.localPath, lineno)
+
+ case "c":
+ // Copy using OSC 52
+ termenv.Copy(m.currentDocument.Body)
+ // Copy using native system clipboard
+ _ = clipboard.WriteAll(m.currentDocument.Body)
+ cmds = append(cmds, m.showStatusMessage(pagerStatusMessage{"Copied contents", false}))
+
+ case "r":
+ return m, loadLocalMarkdown(&m.currentDocument)
+
+ case "?":
+ m.toggleHelp()
+ if m.viewport.HighPerformanceRendering {
+ cmds = append(cmds, viewport.Sync(m.viewport))
+ }
}
// Glow has rendered the content
case contentRenderedMsg:
+ log.Info("content rendered", "state", m.state)
+
m.setContent(string(msg))
if m.viewport.HighPerformanceRendering {
cmds = append(cmds, viewport.Sync(m.viewport))
}
+ cmds = append(cmds, m.watchFile)
+
+ // The file was changed on disk and we're reloading it
+ case reloadMsg:
+ return m, loadLocalMarkdown(&m.currentDocument)
// We've finished editing the document, potentially making changes. Let's
// retrieve the latest version of the document so that we display
@@ -362,69 +264,27 @@ func (m pagerModel) update(msg tea.Msg) (pagerModel, tea.Cmd) {
case editorFinishedMsg:
return m, loadLocalMarkdown(&m.currentDocument)
- // We've reveived terminal dimensions, either for the first time or
+ // We've received terminal dimensions, either for the first time or
// after a resize
case tea.WindowSizeMsg:
return m, renderWithGlamour(m, m.currentDocument.Body)
- case stashSuccessMsg:
- // Stashing was successful. Convert the loaded document to a stashed
- // one and show a status message. Note that we're also handling this
- // message in the main update function where we're adding this stashed
- // item to the stash listing.
- m.state = pagerStateStashSuccess
-
- if !m.spinnerVisible() {
- // The spinner has finished spinning, so tell the user the stash
- // was successful.
- m.state = pagerStateBrowse
- m.currentDocument = markdown(msg)
- cmds = append(cmds, m.showStatusMessage("Stashed!"))
- } else {
- // The spinner is still spinning, so just take note of the newly
- // stashed document for now.
- md := markdown(msg)
- m.stashedDocument = &md
- }
-
- case stashFailMsg:
- delete(m.common.filesStashed, msg.markdown.stashID)
-
case statusMessageTimeoutMsg:
m.state = pagerStateBrowse
}
- switch m.state {
- case pagerStateSetNote:
- m.textInput, cmd = m.textInput.Update(msg)
- cmds = append(cmds, cmd)
- default:
- m.viewport, cmd = m.viewport.Update(msg)
- cmds = append(cmds, cmd)
- }
+ m.viewport, cmd = m.viewport.Update(msg)
+ cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
}
-// spinnerVisible returns whether or not the spinner should be drawn.
-func (m pagerModel) spinnerVisible() bool {
- windowStart := m.spinnerStart.Add(spinnerVisibilityTimeout)
- windowEnd := windowStart.Add(spinnerMinLifetime)
- now := time.Now()
- return now.After(windowStart) && now.Before(windowEnd)
-}
-
func (m pagerModel) View() string {
var b strings.Builder
fmt.Fprint(&b, m.viewport.View()+"\n")
// Footer
- switch m.state {
- case pagerStateSetNote:
- m.setNoteView(&b)
- default:
- m.statusBarView(&b)
- }
+ m.statusBarView(&b)
if m.showHelp {
fmt.Fprint(&b, "\n"+m.helpView())
@@ -440,11 +300,10 @@ func (m pagerModel) statusBarView(b *strings.Builder) {
percentToStringMagnitude float64 = 100.0
)
- isStashed := m.currentDocument.docType == StashedDoc || m.currentDocument.docType == ConvertedDoc
showStatusMessage := m.state == pagerStateStatusMessage
// Logo
- logo := glowLogoView(" Glow ")
+ logo := glowLogoView()
// Scroll percent
percent := math.Max(minPercent, math.Min(maxPercent, m.viewport.ScrollPercent()))
@@ -463,34 +322,16 @@ func (m pagerModel) statusBarView(b *strings.Builder) {
helpNote = statusBarHelpStyle(" ? Help ")
}
- // Status indicator; spinner or stash dot
- var statusIndicator string
- if m.state == pagerStateStashing || m.state == pagerStateStashSuccess {
- var spinner string
- if m.spinnerVisible() {
- spinner = m.spinner.View()
- }
- statusIndicator = statusBarNoteStyle(" ") + spinner
- } else if isStashed && showStatusMessage {
- statusIndicator = statusBarMessageStashIconStyle(" " + pagerStashIcon)
- } else if isStashed {
- statusIndicator = statusBarStashDotStyle(" " + pagerStashIcon)
- }
-
// Note
var note string
if showStatusMessage {
note = m.statusMessage
} else {
note = m.currentDocument.Note
- if len(note) == 0 {
- note = "(No memo)"
- }
}
- note = truncate.StringWithTail(" "+note+" ", uint(max(0,
+ note = truncate.StringWithTail(" "+note+" ", uint(max(0, //nolint:gosec
m.common.width-
ansi.PrintableRuneWidth(logo)-
- ansi.PrintableRuneWidth(statusIndicator)-
ansi.PrintableRuneWidth(scrollPercent)-
ansi.PrintableRuneWidth(helpNote),
)), ellipsis)
@@ -504,7 +345,6 @@ func (m pagerModel) statusBarView(b *strings.Builder) {
padding := max(0,
m.common.width-
ansi.PrintableRuneWidth(logo)-
- ansi.PrintableRuneWidth(statusIndicator)-
ansi.PrintableRuneWidth(note)-
ansi.PrintableRuneWidth(scrollPercent)-
ansi.PrintableRuneWidth(helpNote),
@@ -516,9 +356,8 @@ func (m pagerModel) statusBarView(b *strings.Builder) {
emptySpace = statusBarNoteStyle(emptySpace)
}
- fmt.Fprintf(b, "%s%s%s%s%s%s",
+ fmt.Fprintf(b, "%s%s%s%s%s",
logo,
- statusIndicator,
note,
emptySpace,
scrollPercent,
@@ -526,36 +365,17 @@ func (m pagerModel) statusBarView(b *strings.Builder) {
)
}
-func (m pagerModel) setNoteView(b *strings.Builder) {
- fmt.Fprint(b, noteHeading)
- fmt.Fprint(b, m.textInput.View())
-}
-
func (m pagerModel) helpView() (s string) {
- memoOrStash := "m set memo"
- if m.common.authStatus == authOK && m.currentDocument.docType == LocalDoc {
- memoOrStash = "s stash this document"
- }
-
- editOrBlank := "e edit this document"
- if m.currentDocument.docType != LocalDoc || m.currentDocument.localPath == "" {
- editOrBlank = ""
- }
-
col1 := []string{
"g/home go to top",
"G/end go to bottom",
"c copy contents",
- editOrBlank,
- memoOrStash,
+ "e edit this document",
+ "r reload this document",
"esc back to files",
"q quit",
}
- if m.currentDocument.docType == NewsDoc {
- deleteFromStringSlice(col1, 3)
- }
-
s += "\n"
s += "k/↑ up " + col1[0] + "\n"
s += "j/↓ down " + col1[1] + "\n"
@@ -591,9 +411,7 @@ func renderWithGlamour(m pagerModel, md string) tea.Cmd {
return func() tea.Msg {
s, err := glamourRender(m, md)
if err != nil {
- if debug {
- log.Println("error rendering with Glamour:", err)
- }
+ log.Error("error rendering with Glamour", "error", err)
return errMsg{err}
}
return contentRenderedMsg(s)
@@ -602,53 +420,116 @@ func renderWithGlamour(m pagerModel, md string) tea.Cmd {
// This is where the magic happens.
func glamourRender(m pagerModel, markdown string) (string, error) {
+ trunc := lipgloss.NewStyle().MaxWidth(m.viewport.Width - lineNumberWidth).Render
+
if !config.GlamourEnabled {
return markdown, nil
}
- // initialize glamour
- var gs glamour.TermRendererOption
- if m.common.cfg.GlamourStyle == "auto" {
- gs = glamour.WithAutoStyle()
- } else {
- gs = glamour.WithStylePath(m.common.cfg.GlamourStyle)
+ isCode := !utils.IsMarkdownFile(m.currentDocument.Note)
+ width := max(0, min(int(m.common.cfg.GlamourMaxWidth), m.viewport.Width)) //nolint:gosec
+ if isCode {
+ width = 0
}
- width := max(0, min(int(m.common.cfg.GlamourMaxWidth), m.viewport.Width))
- r, err := glamour.NewTermRenderer(
- gs,
+ options := []glamour.TermRendererOption{
+ utils.GlamourStyle(m.common.cfg.GlamourStyle, isCode),
glamour.WithWordWrap(width),
- )
+ }
+
+ if m.common.cfg.PreserveNewLines {
+ options = append(options, glamour.WithPreservedNewLines())
+ }
+ r, err := glamour.NewTermRenderer(options...)
if err != nil {
- return "", err
+ return "", fmt.Errorf("error creating glamour renderer: %w", err)
+ }
+
+ if isCode {
+ markdown = utils.WrapCodeBlock(markdown, filepath.Ext(m.currentDocument.Note))
}
out, err := r.Render(markdown)
if err != nil {
- return "", err
+ return "", fmt.Errorf("error rendering markdown: %w", err)
+ }
+
+ if isCode {
+ out = strings.TrimSpace(out)
}
// trim lines
lines := strings.Split(out, "\n")
- var content string
+ var content strings.Builder
for i, s := range lines {
- content += strings.TrimSpace(s)
+ if isCode || m.common.cfg.ShowLineNumbers {
+ content.WriteString(lineNumberStyle(fmt.Sprintf("%"+fmt.Sprint(lineNumberWidth)+"d", i+1)))
+ content.WriteString(trunc(s))
+ } else {
+ content.WriteString(s)
+ }
// don't add an artificial newline after the last split
if i+1 < len(lines) {
- content += "\n"
+ content.WriteRune('\n')
}
}
- return content, nil
+ return content.String(), nil
}
-// ETC
-
-// Note: this runs in linear time; O(n).
-func deleteFromStringSlice(a []string, i int) []string {
- copy(a[i:], a[i+1:])
- a[len(a)-1] = ""
- return a[:len(a)-1]
+func (m *pagerModel) initWatcher() {
+ var err error
+ m.watcher, err = fsnotify.NewWatcher()
+ if err != nil {
+ log.Error("error creating fsnotify watcher", "error", err)
+ }
+}
+
+func (m *pagerModel) watchFile() tea.Msg {
+ dir := m.localDir()
+
+ if err := m.watcher.Add(dir); err != nil {
+ log.Error("error adding dir to fsnotify watcher", "error", err)
+ return nil
+ }
+
+ log.Info("fsnotify watching dir", "dir", dir)
+
+ for {
+ select {
+ case event, ok := <-m.watcher.Events:
+ if !ok || event.Name != m.currentDocument.localPath {
+ continue
+ }
+
+ if !event.Has(fsnotify.Write) && !event.Has(fsnotify.Create) {
+ continue
+ }
+
+ log.Debug("fsnotify event", "file", event.Name, "event", event.Op)
+ return reloadMsg{}
+ case err, ok := <-m.watcher.Errors:
+ if !ok {
+ continue
+ }
+ log.Debug("fsnotify error", "dir", dir, "error", err)
+ }
+ }
+}
+
+func (m *pagerModel) unwatchFile() {
+ dir := m.localDir()
+
+ err := m.watcher.Remove(dir)
+ if err == nil {
+ log.Debug("fsnotify dir unwatched", "dir", dir)
+ } else {
+ log.Error("fsnotify fail to unwatch dir", "dir", dir, "error", err)
+ }
+}
+
+func (m *pagerModel) localDir() string {
+ return filepath.Dir(m.currentDocument.localPath)
}
diff --git a/ui/sort.go b/ui/sort.go
new file mode 100644
index 0000000..4f389fb
--- /dev/null
+++ b/ui/sort.go
@@ -0,0 +1,12 @@
+package ui
+
+import (
+ "cmp"
+ "slices"
+)
+
+func sortMarkdowns(mds []*markdown) {
+ slices.SortStableFunc(mds, func(a, b *markdown) int {
+ return cmp.Compare(a.Note, b.Note)
+ })
+}
diff --git a/ui/stash.go b/ui/stash.go
index 7b69d1a..c3739b2 100644
--- a/ui/stash.go
+++ b/ui/stash.go
@@ -3,7 +3,6 @@ package ui
import (
"errors"
"fmt"
- "log"
"os"
"sort"
"strings"
@@ -13,8 +12,8 @@ import (
"github.com/charmbracelet/bubbles/spinner"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
- "github.com/charmbracelet/charm"
"github.com/charmbracelet/lipgloss"
+ "github.com/charmbracelet/log"
"github.com/muesli/reflow/ansi"
"github.com/muesli/reflow/truncate"
"github.com/sahilm/fuzzy"
@@ -28,19 +27,15 @@ const (
stashViewHorizontalPadding = 6
)
-var (
- stashingStatusMessage = statusMessage{normalStatusMessage, "Stashing..."}
- alreadyStashedStatusMessage = statusMessage{subtleStatusMessage, "Already stashed"}
-)
+var stashingStatusMessage = statusMessage{normalStatusMessage, "Stashing..."}
var (
- dividerDot = darkGrayFg(" • ")
- dividerBar = darkGrayFg(" │ ")
- offlineHeaderNote = darkGrayFg("(Offline)")
+ dividerDot = darkGrayFg.SetString(" • ")
+ dividerBar = darkGrayFg.SetString(" │ ")
logoStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#ECFD65")).
- Background(fuschia).
+ Background(fuchsia).
Bold(true)
stashSpinnerStyle = lipgloss.NewStyle().
@@ -49,24 +44,17 @@ var (
Foreground(yellowGreen).
MarginRight(1)
stashInputCursorStyle = lipgloss.NewStyle().
- Foreground(fuschia).
+ Foreground(fuchsia).
MarginRight(1)
)
// MSG
type (
- deletedStashedItemMsg int
- filteredMarkdownMsg []*markdown
- fetchedMarkdownMsg *markdown
+ filteredMarkdownMsg []*markdown
+ fetchedMarkdownMsg *markdown
)
-type markdownFetchFailedMsg struct {
- err error
- id int
- note string
-}
-
// MODEL
// stashViewState is the high-level state of the file listing.
@@ -82,9 +70,7 @@ const (
type sectionKey int
const (
- localSection = iota
- stashedSection
- newsSection
+ documentsSection = iota
filterSection
)
@@ -92,34 +78,12 @@ const (
// its contents in the file listing view.
type section struct {
key sectionKey
- docTypes DocTypeSet
paginator paginator.Model
cursor int
}
// map sections to their associated types.
-var sections = map[sectionKey]section{
- localSection: {
- key: localSection,
- docTypes: NewDocTypeSet(LocalDoc),
- paginator: newStashPaginator(),
- },
- stashedSection: {
- key: stashedSection,
- docTypes: NewDocTypeSet(StashedDoc, ConvertedDoc),
- paginator: newStashPaginator(),
- },
- newsSection: {
- key: newsSection,
- docTypes: NewDocTypeSet(NewsDoc),
- paginator: newStashPaginator(),
- },
- filterSection: {
- key: filterSection,
- docTypes: DocTypeSet{},
- paginator: newStashPaginator(),
- },
-}
+var sections = map[sectionKey]section{}
// filterState is the current filtering state in the file listing.
type filterState int
@@ -130,15 +94,6 @@ const (
filterApplied // a filter is applied and user is not editing filter
)
-// selectionState is the state of the currently selected document.
-type selectionState int
-
-const (
- selectionIdle = iota
- selectionSettingNote
- selectionPromptingDelete
-)
-
// statusMessageType adds some context to the status message being sent.
type statusMessageType int
@@ -155,10 +110,23 @@ type statusMessage struct {
message string
}
+func initSections() {
+ sections = map[sectionKey]section{
+ documentsSection: {
+ key: documentsSection,
+ paginator: newStashPaginator(),
+ },
+ filterSection: {
+ key: filterSection,
+ paginator: newStashPaginator(),
+ },
+ }
+}
+
// String returns a styled version of the status message appropriate for the
// given context.
func (s statusMessage) String() string {
- switch s.status {
+ switch s.status { //nolint:exhaustive
case subtleStatusMessage:
return dimGreenFg(s.message)
case errorStatusMessage:
@@ -172,12 +140,9 @@ type stashModel struct {
common *commonModel
err error
spinner spinner.Model
- noteInput textinput.Model
filterInput textinput.Model
- stashFullyLoaded bool // have we loaded all available stashed documents from the server?
viewState stashViewState
filterState filterState
- selectionState selectionState
showFullHelp bool
showStatusMessage bool
statusMessage statusMessage
@@ -190,8 +155,8 @@ type stashModel struct {
// Index of the section we're currently looking at
sectionIndex int
- // Tracks what exactly is loaded between the stash, news and local files
- loaded DocTypeSet
+ // Tracks if docs were loaded
+ loaded bool
// The master set of markdown documents we're working with.
markdowns []*markdown
@@ -205,19 +170,11 @@ type stashModel struct {
// from the local pagination. Generally, the server will return more items
// than we can display at a time so we can paginate locally without having
// to fetch every time.
- serverPage int
-}
-
-func (m stashModel) localOnly() bool {
- return m.common.cfg.localOnly()
-}
-
-func (m stashModel) stashedOnly() bool {
- return m.common.cfg.stashedOnly()
+ serverPage int64
}
func (m stashModel) loadingDone() bool {
- return m.loaded.Equals(m.common.cfg.DocumentTypes.Difference(ConvertedDoc))
+ return m.loaded
}
func (m stashModel) currentSection() *section {
@@ -240,51 +197,30 @@ func (m *stashModel) setCursor(i int) {
m.currentSection().cursor = i
}
-// Returns whether or not we're online. That is, when "local-only" mode is
-// disabled and we've authenticated successfully.
-func (m stashModel) online() bool {
- return !m.localOnly() && m.common.authStatus == authOK
-}
-
// Whether or not the spinner should be spinning.
func (m stashModel) shouldSpin() bool {
loading := !m.loadingDone()
- stashing := m.common.isStashing()
openingDocument := m.viewState == stashStateLoadingDocument
- return loading || stashing || openingDocument
+ return loading || openingDocument
}
func (m *stashModel) setSize(width, height int) {
m.common.width = width
m.common.height = height
- m.noteInput.Width = width - stashViewHorizontalPadding*2 - ansi.PrintableRuneWidth(m.noteInput.Prompt)
- m.filterInput.Width = width - stashViewHorizontalPadding*2 - ansi.PrintableRuneWidth(m.filterInput.Prompt)
+ m.filterInput.Width = width - stashViewHorizontalPadding*2 - ansi.PrintableRuneWidth(
+ m.filterInput.Prompt,
+ )
m.updatePagination()
}
-// bakeConvertedDocs turns converted documents into stashed ones. Essentially,
-// we're discarding the fact that they were ever converted so we can stop
-// treating them like converted documents.
-func (m *stashModel) bakeConvertedDocs() {
- for _, md := range m.markdowns {
- if md.docType == ConvertedDoc {
- md.docType = StashedDoc
- }
- }
-}
-
func (m *stashModel) resetFiltering() {
m.filterState = unfiltered
m.filterInput.Reset()
m.filteredMarkdowns = nil
- // Turn converted markdowns into stashed ones so that the next time we
- // filter we get both local and stashed results.
- m.bakeConvertedDocs()
-
- sort.Stable(markdownsByLocalFirst(m.markdowns))
+ sortMarkdowns(m.markdowns)
// If the filtered section is present (it's always at the end) slice it out
// of the sections slice to remove it from the UI.
@@ -311,7 +247,7 @@ func (m stashModel) filterApplied() bool {
func (m stashModel) shouldUpdateFilter() bool {
// If we're in the middle of setting a note don't update the filter so that
// the focus won't jump around.
- return m.filterApplied() && m.selectionState != selectionSettingNote
+ return m.filterApplied()
}
// Update pagination according to the amount of markdowns for the current
@@ -357,58 +293,16 @@ func (m stashModel) selectedMarkdown() *markdown {
// Adds markdown documents to the model.
func (m *stashModel) addMarkdowns(mds ...*markdown) {
- if len(mds) > 0 {
- for _, md := range mds {
- md.generateIDs()
- }
-
- m.markdowns = append(m.markdowns, mds...)
- if !m.filterApplied() {
- sort.Stable(markdownsByLocalFirst(m.markdowns))
- }
- m.updatePagination()
- }
-}
-
-// Return the number of markdown documents of a given type.
-func (m stashModel) countMarkdowns(t DocType) (found int) {
- if len(m.markdowns) == 0 {
+ if len(mds) == 0 {
return
}
- var mds []*markdown
- if m.filterState == filtering {
- mds = m.getVisibleMarkdowns()
- } else {
- mds = m.markdowns
+ m.markdowns = append(m.markdowns, mds...)
+ if !m.filterApplied() {
+ sortMarkdowns(m.markdowns)
}
- for i := 0; i < len(mds); i++ {
- if mds[i].docType == t {
- found++
- }
- }
- return
-}
-
-// Sift through the master markdown collection for the specified types.
-func (m stashModel) getMarkdownByType(types ...DocType) []*markdown {
- var agg []*markdown
-
- if len(m.markdowns) == 0 {
- return agg
- }
-
- for _, t := range types {
- for _, md := range m.markdowns {
- if md.docType == t {
- agg = append(agg, md)
- }
- }
- }
-
- sort.Stable(markdownsByLocalFirst(agg))
- return agg
+ m.updatePagination()
}
// Returns the markdowns that should be currently shown.
@@ -417,47 +311,17 @@ func (m stashModel) getVisibleMarkdowns() []*markdown {
return m.filteredMarkdowns
}
- return m.getMarkdownByType(m.currentSection().docTypes.AsSlice()...)
-}
-
-// Return the markdowns eligible to be filtered.
-func (m stashModel) getFilterableMarkdowns() (agg []*markdown) {
- mds := m.getMarkdownByType(LocalDoc, ConvertedDoc, StashedDoc)
-
- // Copy values
- for _, v := range mds {
- p := *v
- agg = append(agg, &p)
- }
-
- return
+ return m.markdowns
}
// Command for opening a markdown document in the pager. Note that this also
// alters the model.
func (m *stashModel) openMarkdown(md *markdown) tea.Cmd {
- var cmd tea.Cmd
m.viewState = stashStateLoadingDocument
-
- if md.docType == LocalDoc {
- cmd = loadLocalMarkdown(md)
- } else {
- cmd = loadRemoteMarkdown(m.common.cc, md)
- }
-
+ cmd := loadLocalMarkdown(md)
return tea.Batch(cmd, m.spinner.Tick)
}
-func (m *stashModel) newStatusMessage(sm statusMessage) tea.Cmd {
- m.showStatusMessage = true
- m.statusMessage = sm
- if m.statusMessageTimer != nil {
- m.statusMessageTimer.Stop()
- }
- m.statusMessageTimer = time.NewTimer(statusMessageTimeout)
- return waitForStatusMessageTimeout(stashContext, m.statusMessageTimer)
-}
-
func (m *stashModel) hideStatusMessage() {
m.showStatusMessage = false
m.statusMessage = statusMessage{}
@@ -514,45 +378,21 @@ func newStashModel(common *commonModel) stashModel {
sp.Spinner = spinner.Line
sp.Style = stashSpinnerStyle
- ni := textinput.New()
- ni.Prompt = "Memo:"
- ni.PromptStyle = stashInputPromptStyle
- ni.CursorStyle = stashInputCursorStyle
- ni.CharLimit = noteCharacterLimit
- ni.Focus()
-
si := textinput.New()
si.Prompt = "Find:"
si.PromptStyle = stashInputPromptStyle
- si.CursorStyle = stashInputCursorStyle
- si.CharLimit = noteCharacterLimit
+ si.Cursor.Style = stashInputCursorStyle
si.Focus()
- var s []section
- if common.cfg.localOnly() {
- s = []section{
- sections[localSection],
- }
- } else if common.cfg.stashedOnly() {
- s = []section{
- sections[stashedSection],
- sections[newsSection],
- }
- } else {
- s = []section{
- sections[localSection],
- sections[stashedSection],
- sections[newsSection],
- }
+ s := []section{
+ sections[documentsSection],
}
m := stashModel{
common: common,
spinner: sp,
- noteInput: ni,
filterInput: si,
serverPage: 1,
- loaded: NewDocTypeSet(),
sections: s,
}
@@ -560,10 +400,10 @@ func newStashModel(common *commonModel) stashModel {
}
func newStashPaginator() paginator.Model {
- p := paginator.NewModel()
+ p := paginator.New()
p.Type = paginator.Dots
p.ActiveDot = brightGrayFg("•")
- p.InactiveDot = darkGrayFg("•")
+ p.InactiveDot = darkGrayFg.Render("•")
return p
}
@@ -576,72 +416,13 @@ func (m stashModel) update(msg tea.Msg) (stashModel, tea.Cmd) {
case errMsg:
m.err = msg
- case stashLoadErrMsg:
- m.err = msg.err
- m.loaded.Add(StashedDoc) // still done, albeit unsuccessfully
- m.stashFullyLoaded = true
-
- case newsLoadErrMsg:
- m.err = msg.err
- m.loaded.Add(NewsDoc) // still done, albeit unsuccessfully
-
case localFileSearchFinished:
// We're finished searching for local files
- m.loaded.Add(LocalDoc)
-
- case gotStashMsg, gotNewsMsg:
- // Stash or news results have come in from the server.
- //
- // With the stash, this doesn't mean the whole stash listing is loaded,
- // but now know it can load, at least, so mark the stash as loaded here.
- var docs []*markdown
-
- switch msg := msg.(type) {
- case gotStashMsg:
- m.loaded.Add(StashedDoc)
- docs = wrapMarkdowns(StashedDoc, msg)
-
- if len(msg) == 0 {
- // If the server comes back with nothing then we've got
- // everything
- m.stashFullyLoaded = true
- } else {
- // Load the next page
- m.serverPage++
- cmds = append(cmds, loadStash(m))
- }
-
- case gotNewsMsg:
- m.loaded.Add(NewsDoc)
- docs = wrapMarkdowns(NewsDoc, msg)
- }
-
- // If we're filtering build filter indexes immediately so any
- // matching results will show up in the filter.
- if m.filterApplied() {
- for _, md := range docs {
- md.buildFilterValue()
- }
- }
- if m.shouldUpdateFilter() {
- cmds = append(cmds, filterMarkdowns(m))
- }
-
- m.addMarkdowns(docs...)
-
- case markdownFetchFailedMsg:
- s := "Couldn't load markdown"
- if msg.note != "" {
- s += ": " + msg.note
- }
- cmd := m.newStatusMessage(statusMessage{
- status: normalStatusMessage,
- message: s,
- })
- return m, cmd
+ m.loaded = true
case filteredMarkdownMsg:
m.filteredMarkdowns = msg
+ m.setCursor(0)
return m, nil
case spinner.TickMsg:
@@ -651,28 +432,6 @@ func (m stashModel) update(msg tea.Msg) (stashModel, tea.Cmd) {
cmds = append(cmds, cmd)
}
- // A note was set on a document. This may have happened in the pager so
- // we'll find the corresponding document here and update accordingly.
- case noteSavedMsg:
- for i := range m.markdowns {
- if m.markdowns[i].ID == msg.ID {
- m.markdowns[i].Note = msg.Note
- }
- }
-
- case stashSuccessMsg:
- // No-op: mechanical stuff related to stash success is handled in the
- // parent update function.
-
- // Note: mechanical stuff related to stash failure is handled in the parent
- // update function.
- case stashFailMsg:
- m.err = msg.err
- cmds = append(cmds, m.newStatusMessage(statusMessage{
- status: errorStatusMessage,
- message: fmt.Sprintf("Couldn’t stash ‘%s’", msg.markdown.Note),
- }))
-
case statusMessageTimeoutMsg:
if applicationContext(msg) == stashContext {
m.hideStatusMessage()
@@ -684,17 +443,8 @@ func (m stashModel) update(msg tea.Msg) (stashModel, tea.Cmd) {
return m, tea.Batch(cmds...)
}
- switch m.selectionState {
- case selectionSettingNote:
- cmds = append(cmds, m.handleNoteInput(msg))
- return m, tea.Batch(cmds...)
- case selectionPromptingDelete:
- cmds = append(cmds, m.handleDeleteConfirmation(msg))
- return m, tea.Batch(cmds...)
- }
-
// Updates per the current state
- switch m.viewState {
+ switch m.viewState { //nolint:exhaustive
case stashStateReady:
cmds = append(cmds, m.handleDocumentBrowsing(msg))
case stashStateShowingError:
@@ -761,17 +511,20 @@ func (m *stashModel) handleDocumentBrowsing(msg tea.Msg) tea.Cmd {
}
m.updatePagination()
+ case "F":
+ m.loaded = false
+ return findLocalFiles(*m.common)
+
// Edit document in EDITOR
case "e":
md := m.selectedMarkdown()
- if md == nil || md.docType != LocalDoc {
- break
+
+ // In case no file is available
+ if md == nil {
+ return nil
}
- file := m.selectedMarkdown().localPath
- if file == "" {
- break
- }
- return openEditor(file)
+
+ return openEditor(md.localPath, 0)
// Open document
case keyEnter:
@@ -789,14 +542,13 @@ func (m *stashModel) handleDocumentBrowsing(msg tea.Msg) tea.Cmd {
// Filter your notes
case "/":
m.hideStatusMessage()
- m.bakeConvertedDocs()
// Build values we'll filter against
for _, md := range m.markdowns {
md.buildFilterValue()
}
- m.filteredMarkdowns = m.getFilterableMarkdowns()
+ m.filteredMarkdowns = m.markdowns
m.paginator().Page = 0
m.setCursor(0)
@@ -805,99 +557,6 @@ func (m *stashModel) handleDocumentBrowsing(msg tea.Msg) tea.Cmd {
m.filterInput.Focus()
return textinput.Blink
- // Set note
- case "m":
- m.hideStatusMessage()
-
- if numDocs == 0 {
- break
- }
-
- md := m.selectedMarkdown()
- isUserMarkdown := md.docType == StashedDoc || md.docType == ConvertedDoc
- isSettingNote := m.selectionState == selectionSettingNote
- isPromptingDelete := m.selectionState == selectionPromptingDelete
-
- if isUserMarkdown && !isSettingNote && !isPromptingDelete {
- m.selectionState = selectionSettingNote
- m.noteInput.SetValue(md.Note)
- m.noteInput.CursorEnd()
- return textinput.Blink
- }
-
- // Stash
- case "s":
- if numDocs == 0 || !m.online() || m.selectedMarkdown() == nil {
- break
- }
-
- md := m.selectedMarkdown()
-
- // Is this a document we're allowed to stash?
- if !stashableDocTypes.Contains(md.docType) {
- break
- }
-
- // Was this document already stashed?
- if _, alreadyStashed := m.common.filesStashed[md.stashID]; alreadyStashed {
- cmds = append(cmds, m.newStatusMessage(alreadyStashedStatusMessage))
- break
- }
-
- // Is the document missing a stash ID?
- if md.stashID.IsNil() {
- if debug && md.stashID.IsNil() {
- log.Printf("refusing to stash markdown; local ID path is nil: %#v", md)
- }
- break
- }
-
- // Checks passed; perform the stash.
- m.common.filesStashed[md.stashID] = struct{}{}
- m.common.filesStashing[md.stashID] = struct{}{}
- m.common.latestFileStashed = md.stashID
- cmds = append(cmds,
- stashDocument(m.common.cc, *md),
- m.newStatusMessage(stashingStatusMessage),
- )
-
- // If we're stashing a filtered item, optimistically convert the
- // filtered item into a stashed item.
- if m.filterApplied() {
- for _, v := range m.filteredMarkdowns {
- if v.uniqueID == md.uniqueID {
- v.convertToStashed()
- }
- }
- }
-
- // The spinner subtly shows the stash state in a non-optimistic
- // fashion, namely because it was originally implemented this way.
- // Ideally, if this stash succeeds quickly enough, the spinner
- // wouldn't run at all.
- cmds = append(cmds, m.spinner.Tick)
-
- // Prompt for deletion
- case "x":
- m.hideStatusMessage()
-
- validState := m.viewState == stashStateReady &&
- m.selectionState == selectionIdle
-
- if numDocs == 0 && !validState {
- break
- }
-
- md := m.selectedMarkdown()
- if md == nil {
- break
- }
-
- t := md.docType
- if t == StashedDoc || t == ConvertedDoc {
- m.selectionState = selectionPromptingDelete
- }
-
// Toggle full help
case "?":
m.showFullHelp = !m.showFullHelp
@@ -938,90 +597,12 @@ func (m *stashModel) handleDocumentBrowsing(msg tea.Msg) tea.Cmd {
return tea.Batch(cmds...)
}
-// Updates for when a user is being prompted whether or not to delete a
-// markdown item.
-func (m *stashModel) handleDeleteConfirmation(msg tea.Msg) tea.Cmd {
- if msg, ok := msg.(tea.KeyMsg); ok {
- switch msg.String() {
- case "y":
- if m.selectionState != selectionPromptingDelete {
- break
- }
-
- smd := m.selectedMarkdown()
-
- for _, md := range m.markdowns {
- if md.uniqueID != smd.uniqueID {
- continue
- }
-
- // Remove from the things-we-stashed-this-session set
- delete(m.common.filesStashed, md.stashID)
-
- // Delete optimistically and remove the stashed item before
- // we've received a success response.
- mds, err := deleteMarkdown(m.markdowns, md)
- if err == nil {
- m.markdowns = mds
- }
-
- break
- }
-
- // Also optimistically delete from filtered markdowns
- if m.filterApplied() {
- for _, md := range m.filteredMarkdowns {
- if md.uniqueID != smd.uniqueID {
- continue
- }
-
- switch md.docType {
- case ConvertedDoc:
- // If the document was stashed in this session, convert it
- // back to it's original document type
- if md.originalDocType == LocalDoc {
- md.revertFromStashed()
- break
- }
-
- // Other documents fall through and delete as normal
- fallthrough
-
- // Otherwise, remove the document from the listing
- default:
- mds, err := deleteMarkdown(m.filteredMarkdowns, md)
- if err == nil {
- m.filteredMarkdowns = mds
- }
- }
- break
- }
- }
-
- m.selectionState = selectionIdle
- m.updatePagination()
-
- if len(m.filteredMarkdowns) == 0 {
- m.resetFiltering()
- }
-
- return deleteStashedItem(m.common.cc, smd.ID)
-
- // Any other key cancels deletion
- default:
- m.selectionState = selectionIdle
- }
- }
-
- return nil
-}
-
// Updates for when a user is in the filter editing interface.
func (m *stashModel) handleFiltering(msg tea.Msg) tea.Cmd {
var cmds []tea.Cmd
// Handle keys
- if msg, ok := msg.(tea.KeyMsg); ok {
+ if msg, ok := msg.(tea.KeyMsg); ok { //nolint:nestif
switch msg.String() {
case keyEsc:
// Cancel filtering
@@ -1084,53 +665,6 @@ func (m *stashModel) handleFiltering(msg tea.Msg) tea.Cmd {
return tea.Batch(cmds...)
}
-func (m *stashModel) handleNoteInput(msg tea.Msg) tea.Cmd {
- var cmds []tea.Cmd
-
- if msg, ok := msg.(tea.KeyMsg); ok {
- switch msg.String() {
- case keyEsc:
- // Cancel note
- m.noteInput.Reset()
- m.selectionState = selectionIdle
- case keyEnter:
- // Set new note
- md := m.selectedMarkdown()
-
- // If the user is issuing a rename on a newly stashed item in a
- // filtered listing, there's a small chance the user could try and
- // set a note before the stash is complete.
- if md.ID == 0 {
- if debug {
- log.Printf("user attempted to rename, but markdown ID is 0: %v", md)
- }
- return m.newStatusMessage(statusMessage{
- status: subtleStatusMessage,
- message: "Too fast. Try again in a sec.",
- })
- }
-
- newNote := m.noteInput.Value()
- cmd := saveDocumentNote(m.common.cc, md.ID, newNote)
- md.Note = newNote
- m.noteInput.Reset()
- m.selectionState = selectionIdle
- return cmd
- }
- }
-
- if m.shouldUpdateFilter() {
- cmds = append(cmds, filterMarkdowns(*m))
- }
-
- // Update the note text input component
- newNoteInputModel, noteInputCmd := m.noteInput.Update(msg)
- m.noteInput = newNoteInputModel
- cmds = append(cmds, noteInputCmd)
-
- return tea.Batch(cmds...)
-}
-
// VIEW
func (m stashModel) view() string {
@@ -1141,25 +675,14 @@ func (m stashModel) view() string {
case stashStateLoadingDocument:
s += " " + m.spinner.View() + " Loading document..."
case stashStateReady:
-
loadingIndicator := " "
if m.shouldSpin() {
loadingIndicator = m.spinner.View()
}
- var header string
- switch m.selectionState {
- case selectionPromptingDelete:
- header = redFg("Delete this item from your stash? ") + faintRedFg("(y/N)")
- case selectionSettingNote:
- header = yellowFg("Set the memo for this item?")
- }
-
// Only draw the normal header if we're not using the header area for
// something else (like a note or delete prompt).
- if header == "" {
- header = m.headerView()
- }
+ header := m.headerView()
// Rules for the logo, filter and status message.
logoOrFilter := " "
@@ -1168,12 +691,12 @@ func (m stashModel) view() string {
} else if m.filterState == filtering {
logoOrFilter += m.filterInput.View()
} else {
- logoOrFilter += glowLogoView(" Glow ")
+ logoOrFilter += glowLogoView()
if m.showStatusMessage {
logoOrFilter += " " + m.statusMessage.String()
}
}
- logoOrFilter = truncate.StringWithTail(logoOrFilter, uint(m.common.width-1), ellipsis)
+ logoOrFilter = truncate.StringWithTail(logoOrFilter, uint(m.common.width-1), ellipsis) //nolint:gosec
help, helpHeight := m.helpView()
@@ -1204,14 +727,10 @@ func (m stashModel) view() string {
// One could argue, in fact, that using pointers in
// a functional framework is an antipattern and our use of
// pointers in our model should be refactored away.
- var p paginator.Model = *(m.paginator())
+ p := *(m.paginator())
p.Type = paginator.Arabic
pagination = paginationStyle.Render(p.View())
}
-
- // We could also look at m.stashFullyLoaded and add an indicator
- // showing that we don't actually know how many more pages there
- // are.
}
s += fmt.Sprintf(
@@ -1228,46 +747,29 @@ func (m stashModel) view() string {
return "\n" + indent(s, stashIndent)
}
-func glowLogoView(text string) string {
- return logoStyle.Render(text)
+func glowLogoView() string {
+ return logoStyle.Render(" Glow ")
}
func (m stashModel) headerView() string {
- localCount := m.countMarkdowns(LocalDoc)
- stashedCount := m.countMarkdowns(StashedDoc) + m.countMarkdowns(ConvertedDoc)
- newsCount := m.countMarkdowns(NewsDoc)
+ localCount := len(m.markdowns)
var sections []string //nolint:prealloc
// Filter results
if m.filterState == filtering {
- if localCount+stashedCount+newsCount == 0 {
+ if localCount == 0 {
return grayFg("Nothing found.")
}
if localCount > 0 {
sections = append(sections, fmt.Sprintf("%d local", localCount))
}
- if stashedCount > 0 {
- sections = append(sections, fmt.Sprintf("%d stashed", stashedCount))
- }
for i := range sections {
sections[i] = grayFg(sections[i])
}
- return strings.Join(sections, dividerDot)
- }
-
- if m.loadingDone() && len(m.markdowns) == 0 {
- var maybeOffline string
- if m.common.authStatus == authFailed {
- maybeOffline = " " + offlineHeaderNote
- }
-
- if m.stashedOnly() {
- return subtleStyle.Render("Can’t load stash") + maybeOffline
- }
- return subtleStyle.Render("No markdown files found") + maybeOffline
+ return strings.Join(sections, dividerDot.String())
}
// Tabs
@@ -1275,21 +777,9 @@ func (m stashModel) headerView() string {
var s string
switch v.key {
- case localSection:
- if m.stashedOnly() {
- continue
- }
- s = fmt.Sprintf("%d local", localCount)
- case stashedSection:
- if m.localOnly() {
- continue
- }
- s = fmt.Sprintf("%d stashed", stashedCount)
- case newsSection:
- if m.localOnly() {
- continue
- }
- s = fmt.Sprintf("%d news", newsCount)
+ case documentsSection:
+ s = fmt.Sprintf("%d documents", localCount)
+
case filterSection:
s = fmt.Sprintf("%d “%s”", len(m.filteredMarkdowns), m.filterInput.Value())
}
@@ -1302,12 +792,7 @@ func (m stashModel) headerView() string {
sections = append(sections, s)
}
- s := strings.Join(sections, dividerBar)
- if m.common.authStatus == authFailed {
- s += dividerDot + offlineHeaderNote
- }
-
- return s
+ return strings.Join(sections, dividerBar.String())
}
func (m stashModel) populatedView() string {
@@ -1322,28 +807,12 @@ func (m stashModel) populatedView() string {
}
switch m.sections[m.sectionIndex].key {
- case localSection:
+ case documentsSection:
if m.loadingDone() {
- f("No local files found.")
+ f("No files found.")
} else {
f("Looking for local files...")
}
- case stashedSection:
- if m.common.authStatus == authFailed {
- f("Can't load your stash. Are you offline?")
- } else if m.loadingDone() {
- f("Nothing stashed yet.")
- } else {
- f("Loading your stash...")
- }
- case newsSection:
- if m.common.authStatus == authFailed {
- f("Can't load news. Are you offline?")
- } else if m.loadingDone() {
- f("No stashed files found.")
- } else {
- f("Loading your stash...")
- }
case filterSection:
return ""
}
@@ -1380,39 +849,15 @@ func (m stashModel) populatedView() string {
// COMMANDS
-// loadRemoteMarkdown is a command for loading markdown from the server.
-func loadRemoteMarkdown(cc *charm.Client, md *markdown) tea.Cmd {
- return func() tea.Msg {
- newMD, err := fetchMarkdown(cc, md.ID, md.docType)
- if err != nil {
- if debug {
- log.Printf("error loading %s markdown (ID %d, Note: '%s'): %v", md.docType, md.ID, md.Note, err)
- }
- return markdownFetchFailedMsg{
- err: err,
- id: md.ID,
- note: md.Note,
- }
- }
- newMD.stashID = md.stashID
- return fetchedMarkdownMsg(newMD)
- }
-}
-
func loadLocalMarkdown(md *markdown) tea.Cmd {
return func() tea.Msg {
- if md.docType != LocalDoc {
- return errMsg{errors.New("could not load local file: not a local file")}
- }
if md.localPath == "" {
return errMsg{errors.New("could not load file: missing path")}
}
data, err := os.ReadFile(md.localPath)
if err != nil {
- if debug {
- log.Println("error reading local markdown:", err)
- }
+ log.Debug("error reading local file", "error", err)
return errMsg{err}
}
md.Body = string(data)
@@ -1420,27 +865,14 @@ func loadLocalMarkdown(md *markdown) tea.Cmd {
}
}
-func deleteStashedItem(cc *charm.Client, id int) tea.Cmd {
- return func() tea.Msg {
- err := cc.DeleteMarkdown(id)
- if err != nil {
- if debug {
- log.Println("could not delete stashed item:", err)
- }
- return errMsg{err}
- }
- return deletedStashedItemMsg(id)
- }
-}
-
func filterMarkdowns(m stashModel) tea.Cmd {
return func() tea.Msg {
if m.filterInput.Value() == "" || !m.filterApplied() {
- return filteredMarkdownMsg(m.getFilterableMarkdowns()) // return everything
+ return filteredMarkdownMsg(m.markdowns) // return everything
}
targets := []string{}
- mds := m.getFilterableMarkdowns()
+ mds := m.markdowns
for _, t := range mds {
targets = append(targets, t.filterValue)
@@ -1457,55 +889,3 @@ func filterMarkdowns(m stashModel) tea.Cmd {
return filteredMarkdownMsg(filtered)
}
}
-
-// ETC
-
-// fetchMarkdown performs the actual I/O for loading markdown from the sever.
-func fetchMarkdown(cc *charm.Client, id int, t DocType) (*markdown, error) {
- var md *charm.Markdown
- var err error
-
- switch t {
- case StashedDoc, ConvertedDoc:
- md, err = cc.GetStashMarkdown(id)
- case NewsDoc:
- md, err = cc.GetNewsMarkdown(id)
- default:
- err = fmt.Errorf("unknown markdown type: %s", t)
- }
-
- if err != nil {
- return nil, err
- }
-
- return &markdown{
- docType: t,
- Markdown: *md,
- }, nil
-}
-
-// Delete a markdown from a slice of markdowns.
-func deleteMarkdown(markdowns []*markdown, target *markdown) ([]*markdown, error) {
- index := -1
-
- // Operate on a copy to avoid any pointer weirdness
- mds := make([]*markdown, len(markdowns))
- copy(mds, markdowns)
-
- for i, v := range mds {
- if v.uniqueID == target.uniqueID {
- index = i
- break
- }
- }
-
- if index == -1 {
- err := fmt.Errorf("could not find markdown to delete")
- if debug {
- log.Println(err)
- }
- return nil, err
- }
-
- return append(mds[:index], mds[index+1:]...), nil
-}
diff --git a/ui/stashhelp.go b/ui/stashhelp.go
index 18f1dcf..6c0b474 100644
--- a/ui/stashhelp.go
+++ b/ui/stashhelp.go
@@ -15,7 +15,7 @@ type helpEntry struct{ key, val string }
type helpColumn []helpEntry
// newHelpColumn creates a help column from pairs of string arguments
-// represeting keys and values. If the arguments are not even (and therein
+// representing keys and values. If the arguments are not even (and therein
// not every key has a matching value) the function will panic.
func newHelpColumn(pairs ...string) (h helpColumn) {
if len(pairs)%2 != 0 {
@@ -99,18 +99,7 @@ func (m stashModel) helpView() (string, int) {
return m.renderHelp(h)
}
- // Help for when we're interacting with a single document
- switch m.selectionState {
- case selectionSettingNote:
- return m.renderHelp([]string{"enter", "confirm", "esc", "cancel"}, []string{"q", "quit"})
- case selectionPromptingDelete:
- return m.renderHelp([]string{"y", "delete", "n", "cancel"}, []string{"q", "quit"})
- }
-
var (
- isStashed bool
- isStashable bool
- isEditable bool
navHelp []string
filterHelp []string
selectionHelp []string
@@ -119,13 +108,6 @@ func (m stashModel) helpView() (string, int) {
appHelp []string
)
- if numDocs > 0 {
- md := m.selectedMarkdown()
- isStashed = md != nil && md.docType == StashedDoc
- isStashable = md != nil && md.docType == LocalDoc && m.online()
- isEditable = md != nil && md.docType == LocalDoc && md.localPath != ""
- }
-
if numDocs > 0 && m.showFullHelp {
navHelp = []string{"enter", "open", "j/k ↑/↓", "choose"}
}
@@ -143,27 +125,23 @@ func (m stashModel) helpView() (string, int) {
}
// If we're browsing a filtered set
- if m.filterState == filterApplied {
- filterHelp = []string{"/", "edit search", "esc", "clear search"}
+ if m.filterApplied() {
+ filterHelp = []string{"/", "edit search", "esc", "clear filter"}
} else {
filterHelp = []string{"/", "find"}
}
- if isStashed {
- selectionHelp = []string{"x", "delete", "m", "set memo"}
- } else if isStashable {
- selectionHelp = []string{"s", "stash"}
- }
-
- if isEditable {
- editHelp = []string{"e", "edit"}
- }
-
// If there are errors
if m.err != nil {
appHelp = append(appHelp, "!", "errors")
}
+ appHelp = append(appHelp, "r", "refresh")
+
+ if numDocs > 0 {
+ appHelp = append(appHelp, "e", "edit")
+ }
+
appHelp = append(appHelp, "q", "quit")
// Detailed help
@@ -181,19 +159,21 @@ func (m stashModel) helpView() (string, int) {
return m.renderHelp(navHelp, filterHelp, selectionHelp, editHelp, sectionHelp, appHelp)
}
+const minHelpViewHeight = 5
+
// renderHelp returns the rendered help view and associated line height for
// the given groups of help items.
func (m stashModel) renderHelp(groups ...[]string) (string, int) {
if m.showFullHelp {
str := m.fullHelpView(groups...)
numLines := strings.Count(str, "\n") + 1
- return str, numLines
+ return str, max(numLines, minHelpViewHeight)
}
return m.miniHelpView(concatStringSlices(groups...)...), 1
}
// Builds the help view from various sections pieces, truncating it if the view
-// would otherwise wrap to two lines. Help view entires should come in as pairs,
+// would otherwise wrap to two lines. Help view entries should come in as pairs,
// with the first being the key and the second being the help text.
func (m stashModel) miniHelpView(entries ...string) string {
if len(entries) == 0 {
@@ -219,19 +199,13 @@ func (m stashModel) miniHelpView(entries ...string) string {
k := entries[i]
v := entries[i+1]
- switch k {
- case "s":
- k = greenFg(k)
- v = semiDimGreenFg(v)
- default:
- k = grayFg(k)
- v = midGrayFg(v)
- }
+ k = grayFg(k)
+ v = midGrayFg(v)
next = fmt.Sprintf("%s %s", k, v)
if i < len(entries)-2 {
- next += dividerDot
+ next += dividerDot.String()
}
// Only this (and the following) help text items if we have the
diff --git a/ui/stashitem.go b/ui/stashitem.go
index 0206364..0000928 100644
--- a/ui/stashitem.go
+++ b/ui/stashitem.go
@@ -2,47 +2,31 @@ package ui
import (
"fmt"
- "log"
"strings"
"github.com/charmbracelet/lipgloss"
- "github.com/muesli/reflow/ansi"
+ "github.com/charmbracelet/log"
"github.com/muesli/reflow/truncate"
"github.com/sahilm/fuzzy"
)
const (
verticalLine = "│"
- noMemoTitle = "No Memo"
fileListingStashIcon = "• "
)
func stashItemView(b *strings.Builder, m stashModel, index int, md *markdown) {
var (
- truncateTo = uint(m.common.width - stashViewHorizontalPadding*2)
- gutter string
- title = md.Note
- date = md.relativeTime()
- icon = ""
+ truncateTo = uint(m.common.width - stashViewHorizontalPadding*2) //nolint:gosec
+ gutter string
+ title = truncate.StringWithTail(md.Note, truncateTo, ellipsis)
+ date = md.relativeTime()
+ editedBy = ""
+ hasEditedBy = false
+ icon = ""
+ separator = ""
)
- switch md.docType {
- case NewsDoc:
- if title == "" {
- title = "News"
- } else {
- title = truncate.StringWithTail(title, truncateTo, ellipsis)
- }
- case StashedDoc, ConvertedDoc:
- icon = fileListingStashIcon
- if title == "" {
- title = noMemoTitle
- }
- title = truncate.StringWithTail(title, truncateTo-uint(ansi.PrintableRuneWidth(icon)), ellipsis)
- default:
- title = truncate.StringWithTail(title, truncateTo, ellipsis)
- }
-
isSelected := index == m.cursor()
isFiltering := m.filterState == filtering
singleFilteredItem := isFiltering && len(m.getVisibleMarkdowns()) == 1
@@ -50,89 +34,67 @@ func stashItemView(b *strings.Builder, m stashModel, index int, md *markdown) {
// If there are multiple items being filtered don't highlight a selected
// item in the results. If we've filtered down to one item, however,
// highlight that first item since pressing return will open it.
- if isSelected && !isFiltering || singleFilteredItem {
+ if isSelected && !isFiltering || singleFilteredItem { //nolint:nestif
// Selected item
-
- switch m.selectionState {
- case selectionPromptingDelete:
- gutter = faintRedFg(verticalLine)
- icon = faintRedFg(icon)
- title = redFg(title)
- date = faintRedFg(date)
- case selectionSettingNote:
- gutter = dullYellowFg(verticalLine)
- icon = ""
- title = m.noteInput.View()
- date = dullYellowFg(date)
- default:
- if m.common.latestFileStashed == md.stashID &&
- m.statusMessage == stashingStatusMessage {
- gutter = greenFg(verticalLine)
- icon = dimGreenFg(icon)
- title = greenFg(title)
- date = semiDimGreenFg(date)
- } else {
- gutter = dullFuchsiaFg(verticalLine)
- icon = dullFuchsiaFg(icon)
- if m.currentSection().key == filterSection &&
- m.filterState == filterApplied || singleFilteredItem {
- s := lipgloss.NewStyle().Foreground(fuschia)
- title = styleFilteredText(title, m.filterInput.Value(), s, s.Copy().Underline(true))
- } else {
- title = fuchsiaFg(title)
- }
- date = dullFuchsiaFg(date)
- }
- }
- } else {
- // Regular (non-selected) items
-
- gutter = " "
-
- if m.common.latestFileStashed == md.stashID &&
- m.statusMessage == stashingStatusMessage {
+ if m.statusMessage == stashingStatusMessage {
+ gutter = greenFg(verticalLine)
icon = dimGreenFg(icon)
title = greenFg(title)
date = semiDimGreenFg(date)
- } else if md.docType == NewsDoc {
- if isFiltering && m.filterInput.Value() == "" {
- title = dimIndigoFg(title)
- date = dimSubtleIndigoFg(date)
+ editedBy = semiDimGreenFg(editedBy)
+ separator = semiDimGreenFg(separator)
+ } else {
+ gutter = dullFuchsiaFg(verticalLine)
+ if m.currentSection().key == filterSection &&
+ m.filterState == filterApplied || singleFilteredItem {
+ s := lipgloss.NewStyle().Foreground(fuchsia)
+ title = styleFilteredText(title, m.filterInput.Value(), s, s.Underline(true))
} else {
- s := lipgloss.NewStyle().Foreground(indigo)
- title = styleFilteredText(title, m.filterInput.Value(), s, s.Copy().Underline(true))
- date = subtleIndigoFg(date)
+ title = fuchsiaFg(title)
+ icon = fuchsiaFg(icon)
}
+ date = dimFuchsiaFg(date)
+ editedBy = dimDullFuchsiaFg(editedBy)
+ separator = dullFuchsiaFg(separator)
+ }
+ } else {
+ gutter = " "
+ if m.statusMessage == stashingStatusMessage {
+ icon = dimGreenFg(icon)
+ title = greenFg(title)
+ date = semiDimGreenFg(date)
+ editedBy = semiDimGreenFg(editedBy)
+ separator = semiDimGreenFg(separator)
} else if isFiltering && m.filterInput.Value() == "" {
icon = dimGreenFg(icon)
- if title == noMemoTitle {
- title = dimBrightGrayFg(title)
- } else {
- title = dimNormalFg(title)
- }
+ title = dimNormalFg(title)
date = dimBrightGrayFg(date)
+ editedBy = dimBrightGrayFg(editedBy)
+ separator = dimBrightGrayFg(separator)
} else {
icon = greenFg(icon)
- if title == noMemoTitle {
- title = brightGrayFg(title)
- } else {
- s := lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#1a1a1a", Dark: "#dddddd"})
- title = styleFilteredText(title, m.filterInput.Value(), s, s.Copy().Underline(true))
- }
- date = brightGrayFg(date)
+
+ s := lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#1a1a1a", Dark: "#dddddd"})
+ title = styleFilteredText(title, m.filterInput.Value(), s, s.Underline(true))
+ date = grayFg(date)
+ editedBy = midGrayFg(editedBy)
+ separator = brightGrayFg(separator)
}
}
- fmt.Fprintf(b, "%s %s%s\n", gutter, icon, title)
+ fmt.Fprintf(b, "%s %s%s%s%s\n", gutter, icon, separator, separator, title)
fmt.Fprintf(b, "%s %s", gutter, date)
+ if hasEditedBy {
+ fmt.Fprintf(b, " %s", editedBy)
+ }
}
func styleFilteredText(haystack, needles string, defaultStyle, matchedStyle lipgloss.Style) string {
b := strings.Builder{}
normalizedHay, err := normalize(haystack)
- if err != nil && debug {
- log.Printf("error normalizing '%s': %v", haystack, err)
+ if err != nil {
+ log.Error("error normalizing", "haystack", haystack, "error", err)
}
matches := fuzzy.Find(needles, []string{normalizedHay})
diff --git a/ui/styles.go b/ui/styles.go
index 3b3ccca..fd25882 100644
--- a/ui/styles.go
+++ b/ui/styles.go
@@ -1,84 +1,46 @@
package ui
-import . "github.com/charmbracelet/lipgloss" //nolint: revive
+import "github.com/charmbracelet/lipgloss"
// Colors.
var (
- normal = AdaptiveColor{Light: "#1A1A1A", Dark: "#dddddd"}
- normalDim = AdaptiveColor{Light: "#A49FA5", Dark: "#777777"}
- gray = AdaptiveColor{Light: "#909090", Dark: "#626262"}
- midGray = AdaptiveColor{Light: "#B2B2B2", Dark: "#4A4A4A"}
- darkGray = AdaptiveColor{Light: "#DDDADA", Dark: "#3C3C3C"}
- brightGray = AdaptiveColor{Light: "#847A85", Dark: "#979797"}
- dimBrightGray = AdaptiveColor{Light: "#C2B8C2", Dark: "#4D4D4D"}
- indigo = AdaptiveColor{Light: "#5A56E0", Dark: "#7571F9"}
- dimIndigo = AdaptiveColor{Light: "#9498FF", Dark: "#494690"}
- subtleIndigo = AdaptiveColor{Light: "#7D79F6", Dark: "#514DC1"}
- dimSubtleIndigo = AdaptiveColor{Light: "#BBBDFF", Dark: "#383584"}
- cream = AdaptiveColor{Light: "#FFFDF5", Dark: "#FFFDF5"}
- yellowGreen = AdaptiveColor{Light: "#04B575", Dark: "#ECFD65"}
- dullYellowGreen = AdaptiveColor{Light: "#6BCB94", Dark: "#9BA92F"}
- fuschia = AdaptiveColor{Light: "#EE6FF8", Dark: "#EE6FF8"}
- dimFuchsia = AdaptiveColor{Light: "#F1A8FF", Dark: "#99519E"}
- dullFuchsia = AdaptiveColor{Dark: "#AD58B4", Light: "#F793FF"}
- dimDullFuchsia = AdaptiveColor{Light: "#F6C9FF", Dark: "#6B3A6F"}
- green = Color("#04B575")
- red = AdaptiveColor{Light: "#FF4672", Dark: "#ED567A"}
- faintRed = AdaptiveColor{Light: "#FF6F91", Dark: "#C74665"}
-
- semiDimGreen = AdaptiveColor{Light: "#35D79C", Dark: "#036B46"}
- dimGreen = AdaptiveColor{Light: "#72D2B0", Dark: "#0B5137"}
+ normalDim = lipgloss.AdaptiveColor{Light: "#A49FA5", Dark: "#777777"}
+ gray = lipgloss.AdaptiveColor{Light: "#909090", Dark: "#626262"}
+ midGray = lipgloss.AdaptiveColor{Light: "#B2B2B2", Dark: "#4A4A4A"}
+ darkGray = lipgloss.AdaptiveColor{Light: "#DDDADA", Dark: "#3C3C3C"}
+ brightGray = lipgloss.AdaptiveColor{Light: "#847A85", Dark: "#979797"}
+ dimBrightGray = lipgloss.AdaptiveColor{Light: "#C2B8C2", Dark: "#4D4D4D"}
+ cream = lipgloss.AdaptiveColor{Light: "#FFFDF5", Dark: "#FFFDF5"}
+ yellowGreen = lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#ECFD65"}
+ fuchsia = lipgloss.AdaptiveColor{Light: "#EE6FF8", Dark: "#EE6FF8"}
+ dimFuchsia = lipgloss.AdaptiveColor{Light: "#F1A8FF", Dark: "#99519E"}
+ dullFuchsia = lipgloss.AdaptiveColor{Dark: "#AD58B4", Light: "#F793FF"}
+ dimDullFuchsia = lipgloss.AdaptiveColor{Light: "#F6C9FF", Dark: "#7B4380"}
+ green = lipgloss.Color("#04B575")
+ red = lipgloss.AdaptiveColor{Light: "#FF4672", Dark: "#ED567A"}
+ semiDimGreen = lipgloss.AdaptiveColor{Light: "#35D79C", Dark: "#036B46"}
+ dimGreen = lipgloss.AdaptiveColor{Light: "#72D2B0", Dark: "#0B5137"}
)
// Ulimately, we'll transition to named styles.
-// nolint:deadcode,unused,varcheck
var (
- normalFg = NewStyle().Foreground(normal).Render
- dimNormalFg = NewStyle().Foreground(normalDim).Render
-
- brightGrayFg = NewStyle().Foreground(brightGray).Render
- dimBrightGrayFg = NewStyle().Foreground(dimBrightGray).Render
-
- grayFg = NewStyle().Foreground(gray).Render
- midGrayFg = NewStyle().Foreground(midGray).Render
- darkGrayFg = NewStyle().Foreground(darkGray).Render
-
- greenFg = NewStyle().Foreground(green).Render
- semiDimGreenFg = NewStyle().Foreground(semiDimGreen).Render
- dimGreenFg = NewStyle().Foreground(dimGreen).Render
-
- fuchsiaFg = NewStyle().Foreground(fuschia).Render
- dimFuchsiaFg = NewStyle().Foreground(dimFuchsia).Render
-
- dullFuchsiaFg = NewStyle().Foreground(dullFuchsia).Render
- dimDullFuchsiaFg = NewStyle().Foreground(dimDullFuchsia).Render
-
- indigoFg = NewStyle().Foreground(fuschia).Render
- dimIndigoFg = NewStyle().Foreground(dimIndigo).Render
-
- subtleIndigoFg = NewStyle().Foreground(subtleIndigo).Render
- dimSubtleIndigoFg = NewStyle().Foreground(dimSubtleIndigo).Render
-
- yellowFg = NewStyle().Foreground(yellowGreen).Render // renders light green on light backgrounds
- dullYellowFg = NewStyle().Foreground(dullYellowGreen).Render // renders light green on light backgrounds
- redFg = NewStyle().Foreground(red).Render
- faintRedFg = NewStyle().Foreground(faintRed).Render
-)
-
-var (
- tabStyle = NewStyle().
- Foreground(AdaptiveColor{Light: "#909090", Dark: "#626262"})
-
- selectedTabStyle = NewStyle().
- Foreground(AdaptiveColor{Light: "#333333", Dark: "#979797"})
-
- errorTitleStyle = NewStyle().
- Foreground(cream).
- Background(red).
- Padding(0, 1)
-
- subtleStyle = NewStyle().
- Foreground(AdaptiveColor{Light: "#9B9B9B", Dark: "#5C5C5C"})
-
- paginationStyle = subtleStyle.Copy()
+ dimNormalFg = lipgloss.NewStyle().Foreground(normalDim).Render
+ brightGrayFg = lipgloss.NewStyle().Foreground(brightGray).Render
+ dimBrightGrayFg = lipgloss.NewStyle().Foreground(dimBrightGray).Render
+ grayFg = lipgloss.NewStyle().Foreground(gray).Render
+ midGrayFg = lipgloss.NewStyle().Foreground(midGray).Render
+ darkGrayFg = lipgloss.NewStyle().Foreground(darkGray)
+ greenFg = lipgloss.NewStyle().Foreground(green).Render
+ semiDimGreenFg = lipgloss.NewStyle().Foreground(semiDimGreen).Render
+ dimGreenFg = lipgloss.NewStyle().Foreground(dimGreen).Render
+ fuchsiaFg = lipgloss.NewStyle().Foreground(fuchsia).Render
+ dimFuchsiaFg = lipgloss.NewStyle().Foreground(dimFuchsia).Render
+ dullFuchsiaFg = lipgloss.NewStyle().Foreground(dullFuchsia).Render
+ dimDullFuchsiaFg = lipgloss.NewStyle().Foreground(dimDullFuchsia).Render
+ redFg = lipgloss.NewStyle().Foreground(red).Render
+ tabStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#909090", Dark: "#626262"})
+ selectedTabStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#333333", Dark: "#979797"})
+ errorTitleStyle = lipgloss.NewStyle().Foreground(cream).Background(red).Padding(0, 1)
+ subtleStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#9B9B9B", Dark: "#5C5C5C"})
+ paginationStyle = subtleStyle
)
diff --git a/ui/ui.go b/ui/ui.go
index 478a9d7..3537d3f 100644
--- a/ui/ui.go
+++ b/ui/ui.go
@@ -1,33 +1,24 @@
+// Package ui provides the main UI for the glow application.
package ui
import (
- "errors"
"fmt"
- "log"
"os"
"path/filepath"
"strings"
"time"
tea "github.com/charmbracelet/bubbletea"
- "github.com/charmbracelet/charm"
- "github.com/charmbracelet/charm/keygen"
- "github.com/charmbracelet/glow/utils"
+ "github.com/charmbracelet/glamour/styles"
+ "github.com/charmbracelet/glow/v2/utils"
+ "github.com/charmbracelet/log"
"github.com/muesli/gitcha"
te "github.com/muesli/termenv"
- "github.com/segmentio/ksuid"
)
const (
- noteCharacterLimit = 256 // should match server
- statusMessageTimeout = time.Second * 2 // how long to show status messages like "stashed!"
+ statusMessageTimeout = time.Second * 3 // how long to show status messages like "stashed!"
ellipsis = "…"
-
- // Only show the spinner if it spins for at least this amount of time.
- spinnerVisibilityTimeout = time.Millisecond * 140
-
- // Minimum amount of time the spinner should be visible once it starts.
- spinnerMinLifetime = time.Millisecond * 550
)
var (
@@ -36,29 +27,25 @@ var (
markdownExtensions = []string{
"*.md", "*.mdown", "*.mkdn", "*.mkd", "*.markdown",
}
-
- // True if we're logging to a file, in which case we'll log more stuff.
- debug = false
-
- // Types of documents we allow the user to stash.
- stashableDocTypes = NewDocTypeSet(LocalDoc, NewsDoc)
)
// NewProgram returns a new Tea program.
-func NewProgram(cfg Config) *tea.Program {
- if cfg.Logfile != "" {
- log.Println("-- Starting Glow ----------------")
- log.Printf("High performance pager: %v", cfg.HighPerformancePager)
- log.Printf("Glamour rendering: %v", cfg.GlamourEnabled)
- log.Println("Bubble Tea now initializing...")
- debug = true
- }
+func NewProgram(cfg Config, content string) *tea.Program {
+ log.Debug(
+ "Starting glow",
+ "high_perf_pager",
+ cfg.HighPerformancePager,
+ "glamour",
+ cfg.GlamourEnabled,
+ )
+
config = cfg
opts := []tea.ProgramOption{tea.WithAltScreen()}
if cfg.EnableMouse {
opts = append(opts, tea.WithMouseCellMotion())
}
- return tea.NewProgram(newModel(cfg), opts...)
+ m := newModel(cfg, content)
+ return tea.NewProgram(m, opts...)
}
type errMsg struct{ err error }
@@ -66,10 +53,6 @@ type errMsg struct{ err error }
func (e errMsg) Error() string { return e.err.Error() }
type (
- newCharmClientMsg *charm.Client
- sshAuthErrMsg struct{}
- keygenFailedMsg struct{ err error }
- keygenSuccessMsg struct{}
initLocalFileSearchMsg struct {
cwd string
ch chan gitcha.SearchResult
@@ -79,19 +62,10 @@ type (
type (
foundLocalFileMsg gitcha.SearchResult
localFileSearchFinished struct{}
- gotStashMsg []*charm.Markdown
- stashLoadErrMsg struct{ err error }
- gotNewsMsg []*charm.Markdown
statusMessageTimeoutMsg applicationContext
- newsLoadErrMsg struct{ err error }
- stashSuccessMsg markdown
- stashFailMsg struct {
- err error
- markdown markdown
- }
)
-// applicationContext indicates the area of the application something appies
+// applicationContext indicates the area of the application something applies
// to. Occasionally used as an argument to commands and messages.
type applicationContext int
@@ -115,60 +89,18 @@ func (s state) String() string {
}[s]
}
-type authStatus int
-
-const (
- authConnecting authStatus = iota
- authOK
- authFailed
-)
-
-func (s authStatus) String() string {
- return map[authStatus]string{
- authConnecting: "connecting",
- authOK: "ok",
- authFailed: "failed",
- }[s]
-}
-
-type keygenState int
-
-const (
- keygenUnstarted keygenState = iota
- keygenRunning
- keygenFinished
-)
-
// Common stuff we'll need to access in all models.
type commonModel struct {
- cfg Config
- cc *charm.Client
- cwd string
- authStatus authStatus
- width int
- height int
-
- // Local IDs of files stashed this session. We treat this like a set,
- // ignoring the value portion with an empty struct.
- filesStashed map[ksuid.KSUID]struct{}
-
- // ID of the most recently stashed markdown
- latestFileStashed ksuid.KSUID
-
- // Files currently being stashed. We remove files from this set once
- // a stash operation has either succeeded or failed.
- filesStashing map[ksuid.KSUID]struct{}
-}
-
-func (c commonModel) isStashing() bool {
- return len(c.filesStashing) > 0
+ cfg Config
+ cwd string
+ width int
+ height int
}
type model struct {
- common *commonModel
- state state
- keygenState keygenState
- fatalErr error
+ common *commonModel
+ state state
+ fatalErr error
// Sub-models
stash stashModel
@@ -189,7 +121,7 @@ func (m *model) unloadDocument() []tea.Cmd {
var batch []tea.Cmd
if m.pager.viewport.HighPerformanceRendering {
- batch = append(batch, tea.ClearScrollArea)
+ batch = append(batch, tea.ClearScrollArea) //nolint:staticcheck
}
if !m.stash.shouldSpin() {
@@ -198,48 +130,73 @@ func (m *model) unloadDocument() []tea.Cmd {
return batch
}
-func newModel(cfg Config) tea.Model {
- if cfg.GlamourStyle == "auto" {
+func newModel(cfg Config, content string) tea.Model {
+ initSections()
+
+ if cfg.GlamourStyle == styles.AutoStyle {
if te.HasDarkBackground() {
- cfg.GlamourStyle = "dark"
+ cfg.GlamourStyle = styles.DarkStyle
} else {
- cfg.GlamourStyle = "light"
+ cfg.GlamourStyle = styles.LightStyle
}
}
- if len(cfg.DocumentTypes) == 0 {
- cfg.DocumentTypes.Add(LocalDoc, StashedDoc, ConvertedDoc, NewsDoc)
- }
-
common := commonModel{
- cfg: cfg,
- authStatus: authConnecting,
- filesStashed: make(map[ksuid.KSUID]struct{}),
- filesStashing: make(map[ksuid.KSUID]struct{}),
+ cfg: cfg,
}
- return model{
- common: &common,
- state: stateShowStash,
- keygenState: keygenUnstarted,
- pager: newPagerModel(&common),
- stash: newStashModel(&common),
+ m := model{
+ common: &common,
+ state: stateShowStash,
+ pager: newPagerModel(&common),
+ stash: newStashModel(&common),
}
+
+ path := cfg.Path
+ if path == "" && content != "" {
+ m.state = stateShowDocument
+ m.pager.currentDocument = markdown{Body: content}
+ return m
+ }
+
+ if path == "" {
+ path = "."
+ }
+ info, err := os.Stat(path)
+ if err != nil {
+ log.Error("unable to stat file", "file", path, "error", err)
+ m.fatalErr = err
+ return m
+ }
+ if info.IsDir() {
+ m.state = stateShowStash
+ } else {
+ cwd, _ := os.Getwd()
+ m.state = stateShowDocument
+ m.pager.currentDocument = markdown{
+ localPath: path,
+ Note: stripAbsolutePath(path, cwd),
+ Modtime: info.ModTime(),
+ }
+ }
+
+ return m
}
func (m model) Init() tea.Cmd {
- var cmds []tea.Cmd
- d := m.common.cfg.DocumentTypes
+ cmds := []tea.Cmd{m.stash.spinner.Tick}
- if d.Contains(StashedDoc) || d.Contains(NewsDoc) {
- cmds = append(cmds,
- newCharmClient,
- m.stash.spinner.Tick,
- )
- }
-
- if d.Contains(LocalDoc) {
- cmds = append(cmds, findLocalFiles(m))
+ switch m.state {
+ case stateShowStash:
+ cmds = append(cmds, findLocalFiles(*m.common))
+ case stateShowDocument:
+ content, err := os.ReadFile(m.common.cfg.Path)
+ if err != nil {
+ log.Error("unable to read file", "file", m.common.cfg.Path, "error", err)
+ return func() tea.Msg { return errMsg{err} }
+ }
+ body := string(utils.RemoveFrontmatter(content))
+ cmds = append(cmds, renderWithGlamour(m.pager, body))
}
return tea.Batch(cmds...)
@@ -259,43 +216,45 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.KeyMsg:
switch msg.String() {
case "esc":
- if m.state == stateShowDocument {
+ if m.state == stateShowDocument || m.stash.viewState == stashStateLoadingDocument {
batch := m.unloadDocument()
return m, tea.Batch(batch...)
}
+ case "r":
+ var cmd tea.Cmd
+ if m.state == stateShowStash {
+ // pass through all keys if we're editing the filter
+ if m.stash.filterState == filtering {
+ m.stash, cmd = m.stash.update(msg)
+ return m, cmd
+ }
+ m.stash.markdowns = nil
+ return m, m.Init()
+ }
case "q":
var cmd tea.Cmd
- switch m.state {
+ switch m.state { //nolint:exhaustive
case stateShowStash:
// pass through all keys if we're editing the filter
- if m.stash.filterState == filtering || m.stash.selectionState == selectionSettingNote {
+ if m.stash.filterState == filtering {
m.stash, cmd = m.stash.update(msg)
return m, cmd
}
-
- // Special cases for the pager
- case stateShowDocument:
- switch m.pager.state {
- // If setting a note send all keys straight through
- case pagerStateSetNote:
- var batch []tea.Cmd
- newPagerModel, cmd := m.pager.update(msg)
- m.pager = newPagerModel
- batch = append(batch, cmd)
- return m, tea.Batch(batch...)
- }
}
return m, tea.Quit
case "left", "h", "delete":
- if m.state == stateShowDocument && m.pager.state != pagerStateSetNote {
+ if m.state == stateShowDocument {
cmds = append(cmds, m.unloadDocument()...)
return m, tea.Batch(cmds...)
}
+ case "ctrl+z":
+ return m, tea.Suspend
+
// Ctrl+C always quits no matter where in the application you are.
case "ctrl+c":
return m, tea.Quit
@@ -313,66 +272,16 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.common.cwd = msg.cwd
cmds = append(cmds, findNextLocalFile(m))
- case sshAuthErrMsg:
- if m.keygenState != keygenFinished { // if we haven't run the keygen yet, do that
- m.keygenState = keygenRunning
- cmds = append(cmds, generateSSHKeys)
- } else {
- // The keygen ran but things still didn't work and we can't auth
- m.common.authStatus = authFailed
- m.stash.err = errors.New("SSH authentication failed; we tried ssh-agent, loading keys from disk, and generating SSH keys")
- if debug {
- log.Println("entering offline mode;", m.stash.err)
- }
-
- // Even though it failed, news/stash loading is finished
- m.stash.loaded.Add(StashedDoc, NewsDoc)
- }
-
- case keygenFailedMsg:
- // Keygen failed. That sucks.
- m.common.authStatus = authFailed
- m.stash.err = errors.New("could not authenticate; could not generate SSH keys")
- if debug {
- log.Println("entering offline mode;", m.stash.err)
- }
-
- m.keygenState = keygenFinished
-
- // Even though it failed, news/stash loading is finished
- m.stash.loaded.Add(StashedDoc, NewsDoc)
-
- case keygenSuccessMsg:
- // The keygen's done, so let's try initializing the charm client again
- m.keygenState = keygenFinished
- cmds = append(cmds, newCharmClient)
-
- case newCharmClientMsg:
- m.common.cc = msg
- m.common.authStatus = authOK
- cmds = append(cmds, loadStash(m.stash), loadNews(m.stash))
-
- case stashLoadErrMsg:
- m.common.authStatus = authFailed
-
case fetchedMarkdownMsg:
// We've loaded a markdown file's contents for rendering
m.pager.currentDocument = *msg
- msg.Body = string(utils.RemoveFrontmatter([]byte(msg.Body)))
- cmds = append(cmds, renderWithGlamour(m.pager, msg.Body))
+ body := string(utils.RemoveFrontmatter([]byte(msg.Body)))
+ cmds = append(cmds, renderWithGlamour(m.pager, body))
case contentRenderedMsg:
m.state = stateShowDocument
- case noteSavedMsg:
- // A note was saved to a document. This will have been done in the
- // pager, so we'll need to find the corresponding note in the stash.
- // So, pass the message to the stash for processing.
- stashModel, cmd := m.stash.update(msg)
- m.stash = stashModel
- return m, cmd
-
- case localFileSearchFinished, gotStashMsg, gotNewsMsg:
+ case localFileSearchFinished:
// Always pass these messages to the stash so we can keep it updated
// about network activity, even if the user isn't currently viewing
// the stash.
@@ -391,35 +300,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
cmds = append(cmds, findNextLocalFile(m))
- case stashSuccessMsg:
- // Common handling that should happen regardless of application state
- md := markdown(msg)
- m.stash.addMarkdowns(&md)
- m.common.filesStashed[msg.stashID] = struct{}{}
- delete(m.common.filesStashing, md.stashID)
-
- if m.stash.filterApplied() {
- for _, v := range m.stash.filteredMarkdowns {
- if v.stashID == msg.stashID && v.docType == ConvertedDoc {
- // Add the server-side ID we got back so we can do things
- // like rename and stash it.
- v.ID = msg.ID
-
- // Keep the unique ID in sync so we can do things like
- // delete. Note that the markdown received a new unique ID
- // when it was added to the file listing in
- // stash.addMarkdowns.
- v.uniqueID = md.uniqueID
- break
- }
- }
- }
-
- case stashFailMsg:
- // Common handling that should happen regardless of application state
- delete(m.common.filesStashed, msg.markdown.stashID)
- delete(m.common.filesStashing, msg.markdown.stashID)
-
case filteredMarkdownMsg:
if m.state == stateShowDocument {
newStashModel, cmd := m.stash.update(msg)
@@ -449,7 +329,7 @@ func (m model) View() string {
return errorView(m.fatalErr, true)
}
- switch m.state {
+ switch m.state { //nolint:exhaustive
case stateShowDocument:
return m.pager.View()
default:
@@ -474,10 +354,11 @@ func errorView(err error, fatal bool) string {
// COMMANDS
-func findLocalFiles(m model) tea.Cmd {
+func findLocalFiles(m commonModel) tea.Cmd {
return func() tea.Msg {
+ log.Info("findLocalFiles")
var (
- cwd = m.common.cfg.WorkingDirectory
+ cwd = m.cfg.Path
err error
)
@@ -493,26 +374,22 @@ func findLocalFiles(m model) tea.Cmd {
// Note that this is one error check for both cases above
if err != nil {
- if debug {
- log.Println("error finding local files:", err)
- }
+ log.Error("error finding local files", "error", err)
return errMsg{err}
}
- if debug {
- log.Println("local directory is:", cwd)
+ log.Debug("local directory is", "cwd", cwd)
+
+ // Switch between FindFiles and FindAllFiles to bypass .gitignore rules
+ var ch chan gitcha.SearchResult
+ if m.cfg.ShowAllFiles {
+ ch, err = gitcha.FindAllFilesExcept(cwd, markdownExtensions, nil)
+ } else {
+ ch, err = gitcha.FindFilesExcept(cwd, markdownExtensions, ignorePatterns(m))
}
- var ignore []string
- if !m.common.cfg.ShowAllFiles {
- ignore = ignorePatterns(m)
- }
-
- ch, err := gitcha.FindFilesExcept(cwd, markdownExtensions, ignore)
if err != nil {
- if debug {
- log.Println("error finding local files:", err)
- }
+ log.Error("error finding local files", "error", err)
return errMsg{err}
}
@@ -529,186 +406,11 @@ func findNextLocalFile(m model) tea.Cmd {
return foundLocalFileMsg(res)
}
// We're done
- if debug {
- log.Println("local file search finished")
- }
+ log.Debug("local file search finished")
return localFileSearchFinished{}
}
}
-func newCharmClient() tea.Msg {
- cfg, err := charm.ConfigFromEnv()
- if err != nil {
- return errMsg{err}
- }
-
- cc, err := charm.NewClient(cfg)
- if err == charm.ErrMissingSSHAuth {
- if debug {
- log.Println("missing SSH auth:", err)
- }
- return sshAuthErrMsg{}
- } else if err != nil {
- if debug {
- log.Println("error creating new charm client:", err)
- }
- return errMsg{err}
- }
-
- return newCharmClientMsg(cc)
-}
-
-func loadStash(m stashModel) tea.Cmd {
- return func() tea.Msg {
- if m.common.cc == nil {
- err := errors.New("no charm client")
- if debug {
- log.Println("error loading stash:", err)
- }
- return stashLoadErrMsg{err}
- }
- stash, err := m.common.cc.GetStash(m.serverPage)
- if err != nil {
- if debug {
- if _, ok := err.(charm.ErrAuthFailed); ok {
- log.Println("auth failure while loading stash:", err)
- } else {
- log.Println("error loading stash:", err)
- }
- }
- return stashLoadErrMsg{err}
- }
- if debug {
- log.Println("loaded stash page", m.serverPage)
- }
- return gotStashMsg(stash)
- }
-}
-
-func loadNews(m stashModel) tea.Cmd {
- return func() tea.Msg {
- if m.common.cc == nil {
- err := errors.New("no charm client")
- if debug {
- log.Println("error loading news:", err)
- }
- return newsLoadErrMsg{err}
- }
- news, err := m.common.cc.GetNews(1) // just fetch the first page
- if err != nil {
- if debug {
- log.Println("error loading news:", err)
- }
- return newsLoadErrMsg{err}
- }
- if debug {
- log.Println("fetched news")
- }
- return gotNewsMsg(news)
- }
-}
-
-func generateSSHKeys() tea.Msg {
- if debug {
- log.Println("running keygen...")
- }
- _, err := keygen.NewSSHKeyPair(nil)
- if err != nil {
- if debug {
- log.Println("keygen failed:", err)
- }
- return keygenFailedMsg{err}
- }
- if debug {
- log.Println("keys generated succcessfully")
- }
- return keygenSuccessMsg{}
-}
-
-func saveDocumentNote(cc *charm.Client, id int, note string) tea.Cmd {
- if cc == nil {
- return func() tea.Msg {
- err := errors.New("can't set note; no charm client")
- if debug {
- log.Println("error saving note:", err)
- }
- return errMsg{err}
- }
- }
- return func() tea.Msg {
- if err := cc.SetMarkdownNote(id, note); err != nil {
- if debug {
- log.Println("error saving note:", err)
- }
- return errMsg{err}
- }
- return noteSavedMsg(&charm.Markdown{ID: id, Note: note})
- }
-}
-
-func stashDocument(cc *charm.Client, md markdown) tea.Cmd {
- return func() tea.Msg {
- if cc == nil {
- err := errors.New("can't stash; no charm client")
- if debug {
- log.Println("error stashing document:", err)
- }
- return stashFailMsg{err, md}
- }
-
- // Is the document missing a body? If so, it likely means it needs to
- // be loaded. But...if it turns out the document body really is empty
- // then we'll stash it anyway.
- if len(md.Body) == 0 {
- switch md.docType {
- case LocalDoc:
- data, err := os.ReadFile(md.localPath)
- if err != nil {
- if debug {
- log.Println("error loading document body for stashing:", err)
- }
- return stashFailMsg{err, md}
- }
- md.Body = string(data)
-
- case NewsDoc:
- newMD, err := fetchMarkdown(cc, md.ID, md.docType)
- if err != nil {
- if debug {
- log.Println(err)
- }
- return stashFailMsg{err, md}
- }
- md.Body = newMD.Body
-
- default:
- err := fmt.Errorf("user is attempting to stash an unsupported markdown type: %s", md.docType)
- if debug {
- log.Println(err)
- }
- return stashFailMsg{err, md}
- }
- }
-
- newMd, err := cc.StashMarkdown(md.Note, md.Body)
- if err != nil {
- if debug {
- log.Println("error stashing document:", err)
- }
- return stashFailMsg{err, md}
- }
-
- md.convertToStashed()
-
- // The server sends the whole stashed document back, but we really just
- // need to know the ID so we can operate on this newly stashed
- // markdown.
- md.ID = newMd.ID
-
- return stashSuccessMsg(md)
- }
-}
-
func waitForStatusMessageTimeout(appCtx applicationContext, t *time.Timer) tea.Cmd {
return func() tea.Msg {
<-t.C
@@ -722,20 +424,17 @@ func waitForStatusMessageTimeout(appCtx applicationContext, t *time.Timer) tea.C
// document. Note that we could be doing things like checking if the file is
// a directory, but we trust that gitcha has already done that.
func localFileToMarkdown(cwd string, res gitcha.SearchResult) *markdown {
- md := &markdown{
- docType: LocalDoc,
+ return &markdown{
localPath: res.Path,
- Markdown: charm.Markdown{
- Note: stripAbsolutePath(res.Path, cwd),
- CreatedAt: res.Info.ModTime(),
- },
+ Note: stripAbsolutePath(res.Path, cwd),
+ Modtime: res.Info.ModTime(),
}
-
- return md
}
func stripAbsolutePath(fullPath, cwd string) string {
- return strings.Replace(fullPath, cwd+string(os.PathSeparator), "", -1)
+ fp, _ := filepath.EvalSymlinks(fullPath)
+ cp, _ := filepath.EvalSymlinks(cwd)
+ return strings.ReplaceAll(fp, cp+string(os.PathSeparator), "")
}
// Lightweight version of reflow's indent function.
@@ -751,17 +450,3 @@ func indent(s string, n int) string {
}
return b.String()
}
-
-func min(a, b int) int {
- if a < b {
- return a
- }
- return b
-}
-
-func max(a, b int) int {
- if a > b {
- return a
- }
- return b
-}
diff --git a/url.go b/url.go
new file mode 100644
index 0000000..f716681
--- /dev/null
+++ b/url.go
@@ -0,0 +1,86 @@
+package main
+
+import (
+ "fmt"
+ "net/url"
+ "strings"
+ "sync"
+)
+
+const (
+ protoGithub = "github://"
+ protoGitlab = "gitlab://"
+ protoHTTPS = "https://"
+)
+
+var (
+ githubURL *url.URL
+ gitlabURL *url.URL
+ urlsOnce sync.Once
+)
+
+func init() {
+ urlsOnce.Do(func() {
+ githubURL, _ = url.Parse("https://github.com")
+ gitlabURL, _ = url.Parse("https://gitlab.com")
+ })
+}
+
+func readmeURL(path string) (*source, error) {
+ switch {
+ case strings.HasPrefix(path, protoGithub):
+ if u := githubReadmeURL(path); u != nil {
+ return readmeURL(u.String())
+ }
+ return nil, nil
+ case strings.HasPrefix(path, protoGitlab):
+ if u := gitlabReadmeURL(path); u != nil {
+ return readmeURL(u.String())
+ }
+ return nil, nil
+ }
+
+ if !strings.HasPrefix(path, protoHTTPS) {
+ path = protoHTTPS + path
+ }
+ u, err := url.Parse(path)
+ if err != nil {
+ return nil, fmt.Errorf("unable to parse url: %w", err)
+ }
+
+ switch {
+ case u.Hostname() == githubURL.Hostname():
+ return findGitHubREADME(u)
+ case u.Hostname() == gitlabURL.Hostname():
+ return findGitLabREADME(u)
+ }
+
+ return nil, nil
+}
+
+func githubReadmeURL(path string) *url.URL {
+ path = strings.TrimPrefix(path, protoGithub)
+ parts := strings.Split(path, "/")
+ if len(parts) != 2 {
+ // custom hostnames are not supported yet
+ return nil
+ }
+ u, _ := url.Parse(githubURL.String())
+ return u.JoinPath(path)
+}
+
+func gitlabReadmeURL(path string) *url.URL {
+ path = strings.TrimPrefix(path, protoGitlab)
+ parts := strings.Split(path, "/")
+ if len(parts) != 2 {
+ // custom hostnames are not supported yet
+ return nil
+ }
+ u, _ := url.Parse(gitlabURL.String())
+ return u.JoinPath(path)
+}
+
+func isURL(path string) bool {
+ _, err := url.ParseRequestURI(path)
+ return err == nil && strings.Contains(path, "://")
+}
diff --git a/url_test.go b/url_test.go
new file mode 100644
index 0000000..9683892
--- /dev/null
+++ b/url_test.go
@@ -0,0 +1,30 @@
+package main
+
+import "testing"
+
+func TestURLParser(t *testing.T) {
+ for path, url := range map[string]string{
+ "github.com/charmbracelet/glow": "https://raw.githubusercontent.com/charmbracelet/glow/master/README.md",
+ "github://charmbracelet/glow": "https://raw.githubusercontent.com/charmbracelet/glow/master/README.md",
+ "github://caarlos0/dotfiles.fish": "https://raw.githubusercontent.com/caarlos0/dotfiles.fish/main/README.md",
+ "github://tj/git-extras": "https://raw.githubusercontent.com/tj/git-extras/main/Readme.md",
+ "https://github.com/goreleaser/nfpm": "https://raw.githubusercontent.com/goreleaser/nfpm/main/README.md",
+ "gitlab.com/caarlos0/test": "https://gitlab.com/caarlos0/test/-/raw/master/README.md",
+ "gitlab://caarlos0/test": "https://gitlab.com/caarlos0/test/-/raw/master/README.md",
+ "https://gitlab.com/terrakok/gitlab-client": "https://gitlab.com/terrakok/gitlab-client/-/raw/develop/Readme.md",
+ } {
+ t.Run(path, func(t *testing.T) {
+ t.Skip("test uses network, sometimes fails for no reason")
+ got, err := readmeURL(path)
+ if err != nil {
+ t.Fatalf("expected no error, got %v", err)
+ }
+ if got == nil {
+ t.Fatalf("should not be nil")
+ }
+ if url != got.URL {
+ t.Errorf("expected url for %s to be %s, was %s", path, url, got.URL)
+ }
+ })
+ }
+}
diff --git a/utils/utils.go b/utils/utils.go
index fe38c13..9f33a1e 100644
--- a/utils/utils.go
+++ b/utils/utils.go
@@ -1,9 +1,16 @@
+// Package utils provides utility functions.
package utils
import (
"os"
+ "path/filepath"
"regexp"
+ "strings"
+ "github.com/charmbracelet/glamour"
+ "github.com/charmbracelet/glamour/ansi"
+ "github.com/charmbracelet/glamour/styles"
+ "github.com/charmbracelet/lipgloss"
"github.com/mitchellh/go-homedir"
)
@@ -24,7 +31,7 @@ func detectFrontmatter(c []byte) []int {
return []int{-1, -1}
}
-// Expands tilde and all environment variables from the given path.
+// ExpandPath expands tilde and all environment variables from the given path.
func ExpandPath(path string) string {
s, err := homedir.Expand(path)
if err == nil {
@@ -32,3 +39,75 @@ func ExpandPath(path string) string {
}
return os.ExpandEnv(path)
}
+
+// WrapCodeBlock wraps a string in a code block with the given language.
+func WrapCodeBlock(s, language string) string {
+ return "```" + language + "\n" + s + "```"
+}
+
+var markdownExtensions = []string{
+ ".md", ".mdown", ".mkdn", ".mkd", ".markdown",
+}
+
+// IsMarkdownFile returns whether the filename has a markdown extension.
+func IsMarkdownFile(filename string) bool {
+ ext := filepath.Ext(filename)
+
+ if ext == "" {
+ // By default, assume it's a markdown file.
+ return true
+ }
+
+ for _, v := range markdownExtensions {
+ if strings.EqualFold(ext, v) {
+ return true
+ }
+ }
+
+ // Has an extension but not markdown
+ // so assume this is a code file.
+ return false
+}
+
+// GlamourStyle returns a glamour.TermRendererOption based on the given style.
+func GlamourStyle(style string, isCode bool) glamour.TermRendererOption {
+ if !isCode {
+ if style == styles.AutoStyle {
+ return glamour.WithAutoStyle()
+ }
+ return glamour.WithStylePath(style)
+ }
+
+ // If we are rendering a pure code block, we need to modify the style to
+ // remove the indentation.
+
+ var styleConfig ansi.StyleConfig
+
+ switch style {
+ case styles.AutoStyle:
+ if lipgloss.HasDarkBackground() {
+ styleConfig = styles.DarkStyleConfig
+ } else {
+ styleConfig = styles.LightStyleConfig
+ }
+ case styles.DarkStyle:
+ styleConfig = styles.DarkStyleConfig
+ case styles.LightStyle:
+ styleConfig = styles.LightStyleConfig
+ case styles.PinkStyle:
+ styleConfig = styles.PinkStyleConfig
+ case styles.NoTTYStyle:
+ styleConfig = styles.NoTTYStyleConfig
+ case styles.DraculaStyle:
+ styleConfig = styles.DraculaStyleConfig
+ case styles.TokyoNightStyle:
+ styleConfig = styles.DraculaStyleConfig
+ default:
+ return glamour.WithStylesFromJSONFile(style)
+ }
+
+ var margin uint
+ styleConfig.CodeBlock.Margin = &margin
+
+ return glamour.WithStyles(styleConfig)
+}