diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 5de2df8..0000000 --- a/.editorconfig +++ /dev/null @@ -1,18 +0,0 @@ -# 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/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..d834ea6 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @meowgorithm @muesli diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d944991..c0e481b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,57 +1,20 @@ version: 2 - updates: - package-ecosystem: "gomod" directory: "/" schedule: - interval: "weekly" - day: "monday" - time: "05:00" - timezone: "America/New_York" + interval: "daily" 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" + interval: "daily" 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 adcb449..20a7a22 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,16 +10,3 @@ 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 7fbd40b..d197e45 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -3,17 +3,21 @@ on: [push, pull_request] jobs: coverage: - runs-on: ubuntu-latest + strategy: + matrix: + go-version: [^1] + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} env: GO111MODULE: "on" steps: - - name: Checkout code - uses: actions/checkout@v6 - - name: Install Go - uses: actions/setup-go@v6 + uses: actions/setup-go@v5 with: - go-version: stable + go-version: ${{ matrix.go-version }} + + - name: Checkout code + uses: actions/checkout@v4 - name: Coverage env: diff --git a/.github/workflows/dependabot-sync.yml b/.github/workflows/dependabot-sync.yml deleted file mode 100644 index 9b08259..0000000 --- a/.github/workflows/dependabot-sync.yml +++ /dev/null @@ -1,17 +0,0 @@ -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 fbc2046..1ccf980 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -1,5 +1,3 @@ -# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json - name: goreleaser on: @@ -23,3 +21,5 @@ 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 deleted file mode 100644 index 43f380f..0000000 --- a/.github/workflows/lint-sync.yml +++ /dev/null @@ -1,13 +0,0 @@ -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 1e21125..be5fbb2 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@v6 + - uses: actions/checkout@v4 - name: golangci-lint - uses: golangci/golangci-lint-action@v9.2.0 + uses: golangci/golangci-lint-action@v4 with: # Optional: golangci-lint command line arguments. args: --issues-exit-code=0 diff --git a/.gitignore b/.gitignore index 8a5159a..a9d459b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ glow dist/ .envrc completions/ -manpages/ +manpages/ \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml index 929cb0a..85cfb6d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,44 +1,34 @@ -version: "2" run: tests: false + +issues: + include: + - EXC0001 + - EXC0005 + - EXC0011 + - EXC0012 + - EXC0013 + + max-issues-per-linter: 0 + max-same-issues: 0 + linters: enable: - bodyclose - - exhaustive + - dupl + - exportloopref - goconst - godot - - gomoddirectives + - godox + - goimports - 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 0c539b7..aa0c1ae 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/glow" + github_url: "https://github.com/charmbracelet/soft-serve" 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 1e27a61..e5a2916 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019-2024 Charmbracelet, Inc +Copyright (c) 2019-2023 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 new file mode 100644 index 0000000..6d80864 --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +# 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 cf3bb15..4862ad2 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Render markdown on the CLI, with _pizzazz_!

- Glow UI Demo + Glow UI Demo

## What is it? @@ -20,9 +20,13 @@ 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. Glow will find local markdown files in subdirectories or a local +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 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 @@ -30,64 +34,39 @@ Git repository. ```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 -``` -```bash -# Nix shell -nix-shell -p glow --command glow -``` +# Nix +nix-env -iA nixpkgs.glow -```bash # FreeBSD pkg install glow -``` -```bash # Solus eopkg 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 @@ -105,12 +84,11 @@ 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/v2@latest +go install github.com/charmbracelet/glow@latest ``` -### Build (requires Go 1.21+) +### Build (requires Go 1.13+) ```bash git clone https://github.com/charmbracelet/glow.git @@ -120,10 +98,11 @@ 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. Glow will find local markdown files in the +browse local and stashed markdown. 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. @@ -131,6 +110,15 @@ 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 @@ -150,6 +138,18 @@ 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: @@ -201,39 +201,42 @@ Here's an example config: ```yaml # style name or JSON path (default "auto") style: "light" -# mouse wheel support (TUI-mode only) +# show local files only; no network (TUI-mode only) +local: true +# mouse support (TUI-mode only) mouse: true # use pager to display markdown pager: true -# at which column should we word wrap? +# word-wrap at width 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 ``` -## Contributing +## 🔒 Encryption: How It Works -See [contributing][contribute]. +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. -[contribute]: https://github.com/charmbracelet/glow/contribute +[charmlib]: https://github.com/charmbracelet/charm ## 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 deleted file mode 100644 index 6d8c844..0000000 --- a/Taskfile.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# 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 390a1db..a7e59e6 100644 --- a/config_cmd.go +++ b/config_cmd.go @@ -1,88 +1,82 @@ package main import ( - "errors" "fmt" - "io/fs" "os" "path" "path/filepath" "github.com/charmbracelet/x/editor" + gap "github.com/muesli/go-app-paths" "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 -# show all files, including hidden and ignored. -all: false -` +width: 80` var configCmd = &cobra.Command{ - Use: "config", - Hidden: false, - Short: "Edit the glow config file", - 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(*cobra.Command, []string) error { - if err := ensureConfigFile(); err != nil { + Use: "config", + Hidden: false, + Short: "Edit the glow config file", + 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, + SilenceUsage: true, + RunE: func(_ *cobra.Command, _ []string) error { + 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 return err } c, err := editor.Cmd("Glow", configFile) if err != nil { - return fmt.Errorf("unable to set config file: %w", err) + return fmt.Errorf("could not edit %s: %w", configFile, err) } c.Stdin = os.Stdin c.Stdout = os.Stdout c.Stderr = os.Stderr if err := c.Run(); err != nil { - return fmt.Errorf("unable to run command: %w", err) + return 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 fe862e3..b824cf5 100644 --- a/github.go +++ b/github.go @@ -1,55 +1,47 @@ package main import ( - "encoding/json" "errors" - "fmt" - "io" "net/http" "net/url" "strings" ) -// 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()) +// 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 } - 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 + u, err := url.ParseRequestURI(s) if err != nil { - return nil, fmt.Errorf("unable to get url: %w", err) + return "", false } - body, err := io.ReadAll(res.Body) + 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) if err != nil { - return nil, fmt.Errorf("unable to read http response body: %w", err) + return nil, err } + u.Host = "raw.githubusercontent.com" - var result readme - if err := json.Unmarshal(body, &result); err != nil { - return nil, fmt.Errorf("unable to parse json: %w", err) - } + for _, r := range readmeNames { + v := u + v.Path += "/master/" + r - if res.StatusCode == http.StatusOK { - //nolint:bodyclose + // nolint:bodyclose // it is closed on the caller - resp, err := http.Get(result.DownloadURL) //nolint: noctx + resp, err := http.Get(v.String()) if err != nil { - return nil, fmt.Errorf("unable to get url: %w", err) + return nil, err } if resp.StatusCode == http.StatusOK { - return &source{resp.Body, result.DownloadURL}, nil + return &source{resp.Body, v.String()}, nil } } diff --git a/gitlab.go b/gitlab.go index 68256be..e8c8a57 100644 --- a/gitlab.go +++ b/gitlab.go @@ -1,59 +1,46 @@ package main import ( - "encoding/json" "errors" - "fmt" - "io" "net/http" "net/url" "strings" ) -// 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()) +// 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 } - 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 + u, err := url.ParseRequestURI(s) if err != nil { - return nil, fmt.Errorf("unable to get url: %w", err) + return "", false } - body, err := io.ReadAll(res.Body) + 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) if err != nil { - return nil, fmt.Errorf("unable to read http response body: %w", err) + return nil, err } - var result readme - if err := json.Unmarshal(body, &result); err != nil { - return nil, fmt.Errorf("unable to parse json: %w", err) - } + for _, r := range readmeNames { + v := u + v.Path += "/raw/master/" + r - readmeRawURL := strings.ReplaceAll(result.ReadmeURL, "blob", "raw") - - if res.StatusCode == http.StatusOK { - //nolint:bodyclose + // nolint:bodyclose // it is closed on the caller - resp, err := http.Get(readmeRawURL) //nolint: gosec,noctx + resp, err := http.Get(v.String()) if err != nil { - return nil, fmt.Errorf("unable to get url: %w", err) + return nil, err } if resp.StatusCode == http.StatusOK { - return &source{resp.Body, readmeRawURL}, nil + return &source{resp.Body, v.String()}, nil } } diff --git a/glow_test.go b/glow_test.go index 8743be2..ed4f0cd 100644 --- a/glow_test.go +++ b/glow_test.go @@ -1,9 +1,31 @@ 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 6e05b0d..70928dd 100644 --- a/go.mod +++ b/go.mod @@ -1,75 +1,76 @@ -module github.com/charmbracelet/glow/v2 +module github.com/charmbracelet/glow -go 1.25.12 - -toolchain go1.26.5 +go 1.18 require ( github.com/atotto/clipboard v0.1.4 - 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/charmbracelet/bubbles v0.18.1-0.20240514172444-4491293fd12c + github.com/charmbracelet/bubbletea v0.26.3-0.20240514172421-c69b27d7eeb8 + github.com/charmbracelet/charm v0.8.7 + github.com/charmbracelet/glamour v0.6.0 + github.com/charmbracelet/lipgloss v0.10.1-0.20240514172355-88ecbb398449 + github.com/charmbracelet/x/editor v0.0.0-20231116172829-450eedbca1ab 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/mattn/go-runewidth v0.0.15 + github.com/meowgorithm/babyenv v1.3.1 github.com/mitchellh/go-homedir v1.1.0 - github.com/muesli/gitcha v0.3.0 + github.com/muesli/gitcha v0.2.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/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 + github.com/segmentio/ksuid v1.0.4 + github.com/spf13/cobra v1.8.0 + github.com/spf13/viper v1.14.0 + golang.org/x/sys v0.20.0 + golang.org/x/term v0.15.0 + golang.org/x/text v0.14.0 ) require ( - github.com/alecthomas/chroma/v2 v2.14.0 // indirect + github.com/alecthomas/chroma v0.10.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/calmh/randomart v1.1.0 // indirect + github.com/charmbracelet/x/ansi v0.1.1 // indirect + github.com/charmbracelet/x/input v0.1.0 // indirect + github.com/charmbracelet/x/term v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.1.0 // indirect + github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect + github.com/dlclark/regexp2 v1.4.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/fsnotify/fsnotify v1.6.0 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/gorilla/css v1.0.0 // indirect + github.com/hashicorp/hcl v1.0.0 // 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/magiconair/properties v1.8.6 // indirect + github.com/mattn/go-isatty v0.0.18 // indirect + github.com/microcosm-cc/bluemonday v1.0.21 // indirect + github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a // indirect + github.com/mitchellh/mapstructure v1.5.0 // 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/muesli/sasquatch v0.0.0-20200811221207-66979d92330a // indirect + github.com/muesli/termenv v0.15.2 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.0.5 // 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/spf13/afero v1.9.2 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.4.1 // 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 + github.com/yuin/goldmark v1.5.2 // indirect + github.com/yuin/goldmark-emoji v1.0.1 // indirect + golang.org/x/crypto v0.17.0 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/sync v0.7.0 // indirect + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 0ccd7cd..81799cc 100644 --- a/go.sum +++ b/go.sum @@ -1,177 +1,718 @@ -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= +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/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/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/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/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= +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/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/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= 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/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= +github.com/aymanbagabas/go-osc52 v1.2.1/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/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/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/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/charmbracelet/bubbles v0.7.5/go.mod h1:IRTORFvhEI6OUH7WhN2Ks8Z8miNGimk1BE6cmHijOkM= +github.com/charmbracelet/bubbles v0.18.1-0.20240514172235-c87a626aa317 h1:wfqvjswMgiO4vE2PzLt/bz+MK3an7HLqo93OYKiSx3Q= +github.com/charmbracelet/bubbles v0.18.1-0.20240514172235-c87a626aa317/go.mod h1:RkbLvNDCdo63q1Ub4hTPx5gOC3f2dVs4i2l4/vx9g14= +github.com/charmbracelet/bubbles v0.18.1-0.20240514172444-4491293fd12c h1:IWA/iYgK3qDnIkhlC2Sd7ApzU0JeNm67rixi3xl6wA4= +github.com/charmbracelet/bubbles v0.18.1-0.20240514172444-4491293fd12c/go.mod h1:K+buXQ4vMkpr6ay/ilfTAdNVnQfx8BJCPg22SmOfLkg= +github.com/charmbracelet/bubbletea v0.12.2/go.mod h1:3gZkYELUOiEUOp0bTInkxguucy/xRbGSOcbMs1geLxg= +github.com/charmbracelet/bubbletea v0.23.2/go.mod h1:FaP3WUivcTM0xOKNmhciz60M6I+weYLF76mr1JyI7sM= +github.com/charmbracelet/bubbletea v0.26.3-0.20240514172205-72ce959b78e3 h1:GugYAtdwJBA1e8+y/diVe41cPS8dgdML7+atTeLS4R8= +github.com/charmbracelet/bubbletea v0.26.3-0.20240514172205-72ce959b78e3/go.mod h1:FixrOn+mReyxbVP0qZcyDsHLKeaZbMHf3wZOxLm8wj4= +github.com/charmbracelet/bubbletea v0.26.3-0.20240514172421-c69b27d7eeb8 h1:uq7c8wSq2/wK/KyvQ3KMQGZp0LFrKhOTAnHK6rNSMG0= +github.com/charmbracelet/bubbletea v0.26.3-0.20240514172421-c69b27d7eeb8/go.mod h1:69vZAJlmlVOxh7Y/Ndvr0csqEP2coOgZolwXx6qvPCk= +github.com/charmbracelet/charm v0.8.7 h1:FJ9b7IxWUWHOPR72zS/QJLEqtudOB2Mwfc+Sir0eZR8= +github.com/charmbracelet/charm v0.8.7/go.mod h1:ApJYwJljEjODkOYJgFDzbUqztLrCWQct9zyPD+xcVr4= +github.com/charmbracelet/glamour v0.6.0 h1:wi8fse3Y7nfcabbbDuwolqTqMQPMnVPeZhDM273bISc= +github.com/charmbracelet/glamour v0.6.0/go.mod h1:taqWV4swIMMbWALc0m7AfE9JkPSU8om2538k9ITBxOc= +github.com/charmbracelet/lipgloss v0.10.1-0.20240514172133-9a1d854f90dd h1:y23y/2Sa7Wa3nseIm0KTP1R0BA44QyFhRB/rvJKOyjI= +github.com/charmbracelet/lipgloss v0.10.1-0.20240514172133-9a1d854f90dd/go.mod h1:/WJOlugg8bddqSP52FFiGXWVIfXsO+/+KzOGAT7opnw= +github.com/charmbracelet/lipgloss v0.10.1-0.20240514172355-88ecbb398449 h1:jy8zvXRALm2v8aK4ivo4m1+/E1/p8nejp5hWRudx2VA= +github.com/charmbracelet/lipgloss v0.10.1-0.20240514172355-88ecbb398449/go.mod h1:/WJOlugg8bddqSP52FFiGXWVIfXsO+/+KzOGAT7opnw= +github.com/charmbracelet/x/ansi v0.1.1 h1:CGAduulr6egay/YVbGc8Hsu8deMg1xZ/bkaXTPi1JDk= +github.com/charmbracelet/x/ansi v0.1.1/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/editor v0.0.0-20231116172829-450eedbca1ab h1:95WbogoQheYFuAUy1olU8OgxrHk2K86zA7mSNELiMfU= +github.com/charmbracelet/x/editor v0.0.0-20231116172829-450eedbca1ab/go.mod h1:lrin7iXW742pX5pePBEWhLPDTp53YW15r/Lp4Rcfg0M= +github.com/charmbracelet/x/input v0.1.0 h1:TEsGSfZYQyOtp+STIjyBq6tpRaorH0qpwZUj8DavAhQ= +github.com/charmbracelet/x/input v0.1.0/go.mod h1:ZZwaBxPF7IG8gWWzPUVqHEtWhc1+HXJPNuerJGRGZ28= +github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI= +github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw= +github.com/charmbracelet/x/windows v0.1.0 h1:gTaxdvzDM5oMa/I2ZNF7wN78X/atWemG9Wph7Ika2k4= +github.com/charmbracelet/x/windows v0.1.0/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ= +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/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/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw= +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-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +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.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= -github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +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.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +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/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 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/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +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-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-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +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/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/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/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/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.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +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/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/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/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/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/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/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +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.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.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +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/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +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/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.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 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/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/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-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -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/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/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/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/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +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/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.3.0 h1:+PJkVKrDXVB0VgRn/yVx2CqSVSDGMSepzvohsCrPYtQ= -github.com/muesli/gitcha v0.3.0/go.mod h1:vX3jFL+XcEUq1uY74RCjLSZfAV+ZuvLg70/NGPdXn84= +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/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/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/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/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.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc= +github.com/muesli/termenv v0.14.0/go.mod h1:kG/pF1E7fh949Xhe156crRUrHNyK221IuGO7Ez60Uc8= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/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/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.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= 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/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_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/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/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/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/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 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/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +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/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 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/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/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/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.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +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/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/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +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.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/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.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +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/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/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= +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.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.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.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +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-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-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +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/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= +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/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/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-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-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-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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +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/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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +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-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-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-20191001151750-bb3f8db39f24/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-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-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-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/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-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/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= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +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/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-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-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-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +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= +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/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-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-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +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.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/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= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -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/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.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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= -mvdan.cc/sh/v3 v3.13.1 h1:DP3TfgZhDkT7lerUdnp6PTGKyxxzz6T+cOlY/xEvfWk= -mvdan.cc/sh/v3 v3.13.1/go.mod h1:lXJ8SexMvEVcHCoDvAGLZgFJ9Wsm2sulmoNEXGhYZD0= +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= diff --git a/log.go b/log.go deleted file mode 100644 index a864a71..0000000 --- a/log.go +++ /dev/null @@ -1,40 +0,0 @@ -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 b31ca15..86a089c 100644 --- a/main.go +++ b/main.go @@ -1,11 +1,9 @@ -// Package main provides the entry point for the Glow CLI application. package main import ( "errors" "fmt" "io" - "io/fs" "net/http" "net/url" "os" @@ -13,15 +11,11 @@ import ( "path/filepath" "strings" - "mvdan.cc/sh/v3/shell" - - "github.com/caarlos0/env/v11" + tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/glamour" - "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" + "github.com/charmbracelet/glow/ui" + "github.com/charmbracelet/glow/utils" + "github.com/meowgorithm/babyenv" gap "github.com/muesli/go-app-paths" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -34,34 +28,23 @@ var ( // CommitSHA as provided by goreleaser. CommitSHA = "" - 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 + readmeNames = []string{"README.md", "README"} + configFile string + pager bool + style string + width uint + showAllFiles bool + localOnly 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: true, + SilenceUsage: false, TraverseChildren: true, - 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, + RunE: execute, } ) @@ -79,22 +62,31 @@ func sourceFromArg(arg string) (*source, error) { } // a GitHub or GitLab URL (even without the protocol): - src, err := readmeURL(arg) - if src != nil && err == nil { - // if there's an error, try next methods... + 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 + } return src, nil } // HTTP(S) URLs: - if u, err := url.ParseRequestURI(arg); err == nil && strings.Contains(arg, "://") { //nolint:nestif + if u, err := url.ParseRequestURI(arg); err == nil && strings.Contains(arg, "://") { 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: noctx,bodyclose + resp, err := http.Get(u.String()) // nolint:bodyclose if err != nil { - return nil, fmt.Errorf("unable to get url: %w", err) + return nil, err } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("HTTP status %d", resp.StatusCode) @@ -109,12 +101,9 @@ func sourceFromArg(arg string) (*source, error) { arg = "." } st, err := os.Stat(arg) - if err == nil && st.IsDir() { //nolint:nestif + if err == nil && st.IsDir() { var src *source - _ = filepath.Walk(arg, func(path string, _ os.FileInfo, err error) error { - if err != nil { - return err - } + _ = filepath.Walk(arg, func(path string, info os.FileInfo, err error) error { for _, v := range readmeNames { if strings.EqualFold(filepath.Base(path), v) { r, err := os.Open(path) @@ -139,49 +128,28 @@ func sourceFromArg(arg string) (*source, error) { return nil, errors.New("missing markdown source") } + // a file: r, err := os.Open(arg) - 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 + u, _ := filepath.Abs(arg) + return &source{r, u}, err } 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 err := validateStyle(style); err != nil { - return err + 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 + } } isTerminal := term.IsTerminal(int(os.Stdout.Fd())) @@ -192,28 +160,26 @@ func validateOptions(cmd *cobra.Command) error { } // Detect terminal width - 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 isTerminal && width == 0 && !cmd.Flags().Changed("width") { + w, _, err := term.GetSize(int(os.Stdout.Fd())) + if err == nil { + width = uint(w) + } - if width > 120 { - width = 120 - } - } - if width == 0 { - width = 80 + if width > 120 { + width = 120 } } + if width == 0 { + width = 80 + } return nil } func stdinIsPipe() (bool, error) { stat, err := os.Stdin.Stat() if err != nil { - return false, fmt.Errorf("unable to open file: %w", err) + return false, err } if stat.Mode()&os.ModeCharDevice == 0 || stat.Size() > 0 { return true, nil @@ -222,6 +188,11 @@ 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 { @@ -235,7 +206,7 @@ func execute(cmd *cobra.Command, args []string) error { switch len(args) { // TUI running on cwd case 0: - return runTUI("", "") + return runTUI("", false) // TUI with possible dir argument case 1: @@ -245,7 +216,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, "") + return runTUI(p, false) } } fallthrough @@ -275,7 +246,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 fmt.Errorf("unable to read from reader: %w", err) + return err } b = utils.RemoveFrontmatter(b) @@ -288,106 +259,104 @@ func executeCLI(cmd *cobra.Command, src *source, w io.Writer) error { baseURL = u.String() + "/" } - isCode := !utils.IsMarkdownFile(src.URL) - // initialize glamour + var gs glamour.TermRendererOption + if style == "auto" { + gs = glamour.WithEnvironmentConfig() + } else { + gs = glamour.WithStylePath(style) + } + r, err := glamour.NewTermRenderer( - glamour.WithColorProfile(lipgloss.ColorProfile()), - utils.GlamourStyle(style, isCode), - glamour.WithWordWrap(int(width)), //nolint:gosec + gs, + glamour.WithWordWrap(int(width)), glamour.WithBaseURL(baseURL), glamour.WithPreservedNewLines(), ) if err != nil { - return fmt.Errorf("unable to create renderer: %w", err) + return err } - content := string(b) - ext := filepath.Ext(src.URL) - if isCode { - content = utils.WrapCodeBlock(string(b), ext) - } - - out, err := r.Render(content) + out, err := r.RenderBytes(b) if err != nil { - return fmt.Errorf("unable to render markdown: %w", err) + return err } + // trim lines + lines := strings.Split(string(out), "\n") + var cb strings.Builder + for i, s := range lines { + cb.WriteString(strings.TrimSpace(s)) + + // don't add an artificial newline after the last split + if i+1 < len(lines) { + cb.WriteString("\n") + } + } + content := cb.String() + // display - switch { - case pager || cmd.Flags().Changed("pager"): + if pager || cmd.Flags().Changed("pager") { pagerCmd := os.Getenv("PAGER") if pagerCmd == "" { pagerCmd = "less -r" } - 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) + pa := strings.Split(pagerCmd, " ") + c := exec.Command(pa[0], pa[1:]...) // nolint:gosec + c.Stdin = strings.NewReader(content) c.Stdout = os.Stdout - 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 + return c.Run() } + + fmt.Fprint(w, content) + return nil } -func runTUI(path string, content string) error { +func runTUI(workingDirectory string, stashedOnly bool) error { // Read environment to get debugging stuff - cfg, err := env.ParseAs[ui.Config]() - if err != nil { + var cfg ui.Config + if err := babyenv.Parse(&cfg); err != nil { return fmt.Errorf("error parsing config: %v", err) } - // use style set in env, or auto if unset - if err := validateStyle(cfg.GlamourStyle); err != nil { - cfg.GlamourStyle = style + // 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 } - cfg.Path = path + cfg.WorkingDirectory = workingDirectory + cfg.DocumentTypes = ui.NewDocTypeSet() cfg.ShowAllFiles = showAllFiles - cfg.ShowLineNumbers = showLineNumbers cfg.GlamourMaxWidth = width + cfg.GlamourStyle = style cfg.EnableMouse = mouse - cfg.PreserveNewLines = preserveNewLines + + if stashedOnly { + cfg.DocumentTypes.Add(ui.StashedDoc, ui.NewsDoc) + } else if localOnly { + cfg.DocumentTypes.Add(ui.LocalDoc) + } // Run Bubble Tea program - if _, err := ui.NewProgram(cfg, content).Run(); err != nil { - return fmt.Errorf("unable to run tui program: %w", err) + if _, err := ui.NewProgram(cfg).Run(); err != nil { + return err } return nil } func main() { - 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") @@ -396,78 +365,63 @@ func init() { Version = "unknown (built from source)" } rootCmd.Version = Version - rootCmd.InitDefaultCompletionCmd() + + scope := gap.NewScope(gap.User, "glow") + defaultConfigFile, _ := scope.ConfigPath("glow.yml") // "Glow Classic" cli arguments - rootCmd.PersistentFlags().StringVar(&configFile, "config", "", fmt.Sprintf("config file (default %s)", viper.GetViper().ConfigFileUsed())) + rootCmd.PersistentFlags().StringVar(&configFile, "config", "", fmt.Sprintf("config file (default %s)", defaultConfigFile)) rootCmd.Flags().BoolVarP(&pager, "pager", "p", false, "display with pager") - 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().StringVarP(&style, "style", "s", "auto", "style name or JSON path") + rootCmd.Flags().UintVarP(&width, "width", "w", 0, "word-wrap at width") rootCmd.Flags().BoolVarP(&showAllFiles, "all", "a", false, "show system files and directories (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(&localOnly, "local", "l", false, "show local files only; no network (TUI-mode only)") 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("debug", rootCmd.Flags().Lookup("debug")) + _ = viper.BindPFlag("local", rootCmd.Flags().Lookup("local")) _ = viper.BindPFlag("mouse", rootCmd.Flags().Lookup("mouse")) - _ = 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("style", "auto") viper.SetDefault("width", 0) - viper.SetDefault("all", true) + viper.SetDefault("local", "false") - rootCmd.AddCommand(configCmd, manCmd) + // Stash + stashCmd.PersistentFlags().StringVarP(&memo, "memo", "m", "", "memo/note for stashing") + rootCmd.AddCommand(stashCmd) + + rootCmd.AddCommand(configCmd) } -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) +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") } - 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 { - log.Warn("Could not parse configuration file", "err", err) + fmt.Println("Error parsing config:", err) + os.Exit(1) } } - 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) - } + // fmt.Println("Using config file:", viper.ConfigFileUsed()) } diff --git a/man_cmd.go b/man_cmd.go deleted file mode 100644 index a179ef7..0000000 --- a/man_cmd.go +++ /dev/null @@ -1,29 +0,0 @@ -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 new file mode 100644 index 0000000..efb8bbd --- /dev/null +++ b/stash_cmd.go @@ -0,0 +1,82 @@ +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 b688a4e..81a1c2d 100644 --- a/style.go +++ b/style.go @@ -1,13 +1,13 @@ package main -import "github.com/charmbracelet/lipgloss" +import . "github.com/charmbracelet/lipgloss" //nolint:revive var ( - keyword = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#04B575")). + keyword = NewStyle(). + Foreground(Color("#04B575")). Render - paragraph = lipgloss.NewStyle(). + paragraph = NewStyle(). Width(78). Padding(0, 0, 0, 2). Render diff --git a/ui/config.go b/ui/config.go index 001d5b8..9db2340 100644 --- a/ui/config.go +++ b/ui/config.go @@ -2,19 +2,29 @@ package ui // Config contains TUI-specific configuration. type Config struct { - ShowAllFiles bool - ShowLineNumbers bool - Gopath string `env:"GOPATH"` - HomeDir string `env:"HOME"` - GlamourMaxWidth uint - GlamourStyle string `env:"GLAMOUR_STYLE"` - EnableMouse bool - PreserveNewLines bool + ShowAllFiles bool + Gopath string `env:"GOPATH"` + HomeDir string `env:"HOME"` + GlamourMaxWidth uint + GlamourStyle string + EnableMouse bool - // Working directory or file path - Path string + // Which directory should we start from? + WorkingDirectory string + + // Which document types shall we show? + DocumentTypes DocTypeSet // For debugging the UI - HighPerformancePager bool `env:"GLOW_HIGH_PERFORMANCE_PAGER" envDefault:"true"` - GlamourEnabled bool `env:"GLOW_ENABLE_GLAMOUR" envDefault:"true"` + 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) } diff --git a/ui/consts_unix.go b/ui/consts_unix.go new file mode 100644 index 0000000..57f75dd --- /dev/null +++ b/ui/consts_unix.go @@ -0,0 +1,8 @@ +//go:build !windows +// +build !windows + +package ui + +const ( + pagerStashIcon = "🔒" +) diff --git a/ui/consts_windows.go b/ui/consts_windows.go new file mode 100644 index 0000000..25f9851 --- /dev/null +++ b/ui/consts_windows.go @@ -0,0 +1,7 @@ +// +build windows + +package ui + +const ( + pagerStashIcon = "•" +) diff --git a/ui/doctypes.go b/ui/doctypes.go new file mode 100644 index 0000000..8d2ef14 --- /dev/null +++ b/ui/doctypes.go @@ -0,0 +1,90 @@ +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 new file mode 100644 index 0000000..e882b14 --- /dev/null +++ b/ui/doctypes_test.go @@ -0,0 +1,48 @@ +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 a567c59..ff094fc 100644 --- a/ui/editor.go +++ b/ui/editor.go @@ -7,13 +7,16 @@ import ( type editorFinishedMsg struct{ err error } -func openEditor(path string, lineno int) tea.Cmd { +func openEditor(path string) tea.Cmd { cb := func(err error) tea.Msg { return editorFinishedMsg{err} } - cmd, err := editor.Cmd("Glow", path, editor.LineNumber(uint(lineno))) //nolint:gosec + + editor, err := editor.Cmd("Glow", path) if err != nil { - return func() tea.Msg { return cb(err) } + return func() tea.Msg { + return errMsg{err} + } } - return tea.ExecProcess(cmd, cb) + return tea.ExecProcess(editor, cb) } diff --git a/ui/ignore_darwin.go b/ui/ignore_darwin.go index b69437a..cccd305 100644 --- a/ui/ignore_darwin.go +++ b/ui/ignore_darwin.go @@ -1,14 +1,13 @@ -//go:build darwin // +build darwin package ui import "path/filepath" -func ignorePatterns(m commonModel) []string { +func ignorePatterns(m model) []string { return []string{ - filepath.Join(m.cfg.HomeDir, "Library"), - m.cfg.Gopath, + filepath.Join(m.common.cfg.HomeDir, "Library"), + m.common.cfg.Gopath, "node_modules", ".*", } diff --git a/ui/ignore_general.go b/ui/ignore_general.go index f4dc8be..43c1dd1 100644 --- a/ui/ignore_general.go +++ b/ui/ignore_general.go @@ -3,9 +3,9 @@ package ui -func ignorePatterns(m commonModel) []string { +func ignorePatterns(m model) []string { return []string{ - m.cfg.Gopath, + m.common.cfg.Gopath, "node_modules", ".*", } diff --git a/ui/markdown.go b/ui/markdown.go index c67b1dc..e1d5086 100644 --- a/ui/markdown.go +++ b/ui/markdown.go @@ -1,19 +1,42 @@ package ui import ( - "fmt" + "log" "math" + "path" + "strings" "time" "unicode" - "github.com/charmbracelet/log" + "github.com/charmbracelet/charm" "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 @@ -23,24 +46,102 @@ type markdown struct { // field is ephemeral, and should only be referenced during filtering. filterValue string - Body string - Note string - Modtime time.Time + 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 } // Generate the value we're doing to filter against. func (m *markdown) buildFilterValue() { note, err := normalize(m.Note) if err != nil { - log.Error("error normalizing", "note", m.Note, "error", err) + if debug { + log.Printf("error normalizing '%s': %v", m.Note, 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 unique 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.Modtime) + return relativeTime(m.CreatedAt) } // Normalize text to aid in the filtering process. In particular, we remove @@ -49,10 +150,19 @@ 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) - if err != nil { - return "", fmt.Errorf("error normalizing: %w", err) + 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, + }) } - return out, nil + return m } // Return the time in a human-readable format relative to the current time. diff --git a/ui/pager.go b/ui/pager.go index 5c522e7..2fc2f19 100644 --- a/ui/pager.go +++ b/ui/pager.go @@ -2,29 +2,25 @@ 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 - lineNumberWidth = 4 -) +const statusBarHeight = 1 var ( pagerHelpHeight int @@ -32,7 +28,11 @@ var ( mintGreen = lipgloss.AdaptiveColor{Light: "#89F0CB", Dark: "#89F0CB"} darkGreen = lipgloss.AdaptiveColor{Light: "#1C8760", Dark: "#1C8760"} - lineNumberFg = lipgloss.AdaptiveColor{Light: "#656565", Dark: "#7D7D7D"} + noteHeading = lipgloss.NewStyle(). + Foreground(cream). + Background(green). + Padding(0, 1). + SetString("Set Memo") statusBarNoteFg = lipgloss.AdaptiveColor{Light: "#656565", Dark: "#7D7D7D"} statusBarBg = lipgloss.AdaptiveColor{Light: "#E6E6E6", Dark: "#242424"} @@ -52,11 +52,21 @@ 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). @@ -72,28 +82,47 @@ var ( Background(lipgloss.AdaptiveColor{Light: "#f2f2f2", Dark: "#1B1B1B"}). Render - lineNumberStyle = lipgloss.NewStyle(). - Foreground(lineNumberFg). - 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(fuchsia) ) type ( contentRenderedMsg string - reloadMsg struct{} + noteSavedMsg *charm.Markdown ) type pagerState int const ( pagerStateBrowse pagerState = iota + pagerStateSetNote + pagerStateStashing + pagerStateStashSuccess pagerStateStatusMessage ) type pagerModel struct { - common *commonModel - viewport viewport.Model - state pagerState - showHelp bool + common *commonModel + viewport viewport.Model + state pagerState + showHelp bool + textInput textinput.Model + + spinner spinner.Model + spinnerStart time.Time statusMessage string statusMessageTimer *time.Timer @@ -102,27 +131,45 @@ type pagerModel struct { // it here so we can re-render it on resize. currentDocument markdown - watcher *fsnotify.Watcher + // Newly stashed markdown. We store it here temporarily so we can replace + // currentDocument above after a stash. + stashedDocument *markdown } func newPagerModel(common *commonModel) pagerModel { // Init viewport - vp := viewport.New(0, 0) + vp := viewport.New(common.ctx, 0, 0) vp.YPosition = 0 vp.HighPerformanceRendering = config.HighPerformancePager - m := pagerModel{ - common: common, - state: pagerStateBrowse, - viewport: vp, + // Text input for notes/memos + ti := textinput.New(common.ctx) + ti.Prompt = " > " + ti.PromptStyle = pagerNoteInputPromptStyle + ti.TextStyle = pagerNoteInputStyle + ti.CursorStyle = pagerNoteInputCursorStyle + ti.CharLimit = noteCharacterLimit + ti.Focus() + + // Text input for search + sp := spinner.New(common.ctx) + sp.Style = spinnerStyle + + return pagerModel{ + common: common, + state: pagerStateBrowse, + textInput: ti, + viewport: vp, + spinner: sp, } - 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.String()) - + ansi.PrintableRuneWidth(m.textInput.Prompt) - 1 if m.showHelp { if pagerHelpHeight == 0 { @@ -144,18 +191,13 @@ 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 returned command should be sent back the through the pager +// that the the returned command should be sent back the through the pager // update function. -func (m *pagerModel) showStatusMessage(msg pagerStatusMessage) tea.Cmd { +func (m *pagerModel) showStatusMessage(statusMessage string) tea.Cmd { // Show a success message to the user m.state = pagerStateStatusMessage - m.statusMessage = msg.message + m.statusMessage = statusMessage if m.statusMessageTimer != nil { m.statusMessageTimer.Stop() } @@ -165,7 +207,6 @@ func (m *pagerModel) showStatusMessage(msg pagerStatusMessage) tea.Cmd { } func (m *pagerModel) unload() { - log.Debug("unload") if m.showHelp { m.toggleHelp() } @@ -175,10 +216,10 @@ func (m *pagerModel) unload() { m.state = pagerStateBrowse m.viewport.SetContent("") m.viewport.YOffset = 0 - m.unwatchFile() + m.textInput.Reset() } -func (m pagerModel) update(msg tea.Msg) (pagerModel, tea.Cmd) { +func (m pagerModel) update(ctx tea.Context, msg tea.Msg) (pagerModel, tea.Cmd) { var ( cmd tea.Cmd cmds []tea.Cmd @@ -186,77 +227,134 @@ func (m pagerModel) update(msg tea.Msg) (pagerModel, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: - switch msg.String() { - case "q", keyEsc: - if m.state != pagerStateBrowse { + switch m.state { + case pagerStateSetNote: + switch msg.String() { + case keyEsc: 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 } - 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)) - } + 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 - case "d": - m.viewport.HalfViewDown() - if m.viewport.HighPerformanceRendering { - cmds = append(cmds, viewport.Sync(m.viewport)) + // 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 "u": - m.viewport.HalfViewUp() - if m.viewport.HighPerformanceRendering { - cmds = append(cmds, viewport.Sync(m.viewport)) - } + case spinner.TickMsg: + spinnerMinTimeout := m.spinnerStart. + Add(spinnerVisibilityTimeout). + Add(spinnerMinLifetime) - 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)) - } + 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(ctx, 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!")) } // 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 @@ -264,27 +362,69 @@ func (m pagerModel) update(msg tea.Msg) (pagerModel, tea.Cmd) { case editorFinishedMsg: return m, loadLocalMarkdown(&m.currentDocument) - // We've received terminal dimensions, either for the first time or + // We've reveived 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 } - m.viewport, cmd = m.viewport.Update(msg) - cmds = append(cmds, cmd) + switch m.state { + case pagerStateSetNote: + m.textInput, cmd = m.textInput.Update(ctx, msg) + cmds = append(cmds, cmd) + default: + m.viewport, cmd = m.viewport.Update(ctx, msg) + cmds = append(cmds, cmd) + } return m, tea.Batch(cmds...) } -func (m pagerModel) View() string { +// 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(ctx tea.Context) string { var b strings.Builder - fmt.Fprint(&b, m.viewport.View()+"\n") + fmt.Fprint(&b, m.viewport.View(ctx)+"\n") // Footer - m.statusBarView(&b) + switch m.state { + case pagerStateSetNote: + m.setNoteView(&b) + default: + m.statusBarView(ctx, &b) + } if m.showHelp { fmt.Fprint(&b, "\n"+m.helpView()) @@ -293,17 +433,18 @@ func (m pagerModel) View() string { return b.String() } -func (m pagerModel) statusBarView(b *strings.Builder) { +func (m pagerModel) statusBarView(ctx tea.Context, b *strings.Builder) { const ( minPercent float64 = 0.0 maxPercent float64 = 1.0 percentToStringMagnitude float64 = 100.0 ) + isStashed := m.currentDocument.docType == StashedDoc || m.currentDocument.docType == ConvertedDoc showStatusMessage := m.state == pagerStateStatusMessage // Logo - logo := glowLogoView() + logo := m.common.styles.LogoStyle.Render(" Glow ") // Scroll percent percent := math.Max(minPercent, math.Min(maxPercent, m.viewport.ScrollPercent())) @@ -322,16 +463,34 @@ 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(ctx) + } + 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, //nolint:gosec + note = truncate.StringWithTail(" "+note+" ", uint(max(0, m.common.width- ansi.PrintableRuneWidth(logo)- + ansi.PrintableRuneWidth(statusIndicator)- ansi.PrintableRuneWidth(scrollPercent)- ansi.PrintableRuneWidth(helpNote), )), ellipsis) @@ -345,6 +504,7 @@ 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), @@ -356,8 +516,9 @@ func (m pagerModel) statusBarView(b *strings.Builder) { emptySpace = statusBarNoteStyle(emptySpace) } - fmt.Fprintf(b, "%s%s%s%s%s", + fmt.Fprintf(b, "%s%s%s%s%s%s", logo, + statusIndicator, note, emptySpace, scrollPercent, @@ -365,17 +526,36 @@ func (m pagerModel) statusBarView(b *strings.Builder) { ) } +func (m pagerModel) setNoteView(b *strings.Builder) { + fmt.Fprint(b, noteHeading) + fmt.Fprint(b, m.textInput.View(m.common.ctx)) +} + 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", - "e edit this document", - "r reload this document", + editOrBlank, + memoOrStash, "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" @@ -411,7 +591,9 @@ func renderWithGlamour(m pagerModel, md string) tea.Cmd { return func() tea.Msg { s, err := glamourRender(m, md) if err != nil { - log.Error("error rendering with Glamour", "error", err) + if debug { + log.Println("error rendering with Glamour:", err) + } return errMsg{err} } return contentRenderedMsg(s) @@ -420,116 +602,53 @@ 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 } - isCode := !utils.IsMarkdownFile(m.currentDocument.Note) - width := max(0, min(int(m.common.cfg.GlamourMaxWidth), m.viewport.Width)) //nolint:gosec - if isCode { - width = 0 + // initialize glamour + var gs glamour.TermRendererOption + if m.common.cfg.GlamourStyle == "auto" { + gs = glamour.WithAutoStyle() + } else { + gs = glamour.WithStylePath(m.common.cfg.GlamourStyle) } - options := []glamour.TermRendererOption{ - utils.GlamourStyle(m.common.cfg.GlamourStyle, isCode), + width := max(0, min(int(m.common.cfg.GlamourMaxWidth), m.viewport.Width)) + r, err := glamour.NewTermRenderer( + gs, glamour.WithWordWrap(width), - } - - if m.common.cfg.PreserveNewLines { - options = append(options, glamour.WithPreservedNewLines()) - } - r, err := glamour.NewTermRenderer(options...) + ) if err != nil { - return "", fmt.Errorf("error creating glamour renderer: %w", err) - } - - if isCode { - markdown = utils.WrapCodeBlock(markdown, filepath.Ext(m.currentDocument.Note)) + return "", err } out, err := r.Render(markdown) if err != nil { - return "", fmt.Errorf("error rendering markdown: %w", err) - } - - if isCode { - out = strings.TrimSpace(out) + return "", err } // trim lines lines := strings.Split(out, "\n") - var content strings.Builder + var content string for i, s := range lines { - 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) - } + content += strings.TrimSpace(s) // don't add an artificial newline after the last split if i+1 < len(lines) { - content.WriteRune('\n') + content += "\n" } } - return content.String(), nil + return content, nil } -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) +// 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] } diff --git a/ui/sort.go b/ui/sort.go deleted file mode 100644 index 4f389fb..0000000 --- a/ui/sort.go +++ /dev/null @@ -1,12 +0,0 @@ -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 c3739b2..9d0c78d 100644 --- a/ui/stash.go +++ b/ui/stash.go @@ -3,6 +3,7 @@ package ui import ( "errors" "fmt" + "log" "os" "sort" "strings" @@ -12,8 +13,7 @@ import ( "github.com/charmbracelet/bubbles/spinner" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - "github.com/charmbracelet/log" + "github.com/charmbracelet/charm" "github.com/muesli/reflow/ansi" "github.com/muesli/reflow/truncate" "github.com/sahilm/fuzzy" @@ -27,34 +27,25 @@ const ( stashViewHorizontalPadding = 6 ) -var stashingStatusMessage = statusMessage{normalStatusMessage, "Stashing..."} - var ( - dividerDot = darkGrayFg.SetString(" • ") - dividerBar = darkGrayFg.SetString(" │ ") - - logoStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#ECFD65")). - Background(fuchsia). - Bold(true) - - stashSpinnerStyle = lipgloss.NewStyle(). - Foreground(gray) - stashInputPromptStyle = lipgloss.NewStyle(). - Foreground(yellowGreen). - MarginRight(1) - stashInputCursorStyle = lipgloss.NewStyle(). - Foreground(fuchsia). - MarginRight(1) + stashingStatusMessage = statusMessage{normalStatusMessage, "Stashing..."} + alreadyStashedStatusMessage = statusMessage{subtleStatusMessage, "Already stashed"} ) // MSG type ( - filteredMarkdownMsg []*markdown - fetchedMarkdownMsg *markdown + deletedStashedItemMsg int + filteredMarkdownMsg []*markdown + fetchedMarkdownMsg *markdown ) +type markdownFetchFailedMsg struct { + err error + id int + note string +} + // MODEL // stashViewState is the high-level state of the file listing. @@ -70,7 +61,9 @@ const ( type sectionKey int const ( - documentsSection = iota + localSection = iota + stashedSection + newsSection filterSection ) @@ -78,6 +71,7 @@ const ( // its contents in the file listing view. type section struct { key sectionKey + docTypes DocTypeSet paginator paginator.Model cursor int } @@ -94,6 +88,15 @@ 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 @@ -110,29 +113,41 @@ type statusMessage struct { message string } -func initSections() { +func initSections(c *commonModel) { sections = map[sectionKey]section{ - documentsSection: { - key: documentsSection, - paginator: newStashPaginator(), + localSection: { + key: localSection, + docTypes: NewDocTypeSet(LocalDoc), + paginator: newStashPaginator(c), + }, + stashedSection: { + key: stashedSection, + docTypes: NewDocTypeSet(StashedDoc, ConvertedDoc), + paginator: newStashPaginator(c), + }, + newsSection: { + key: newsSection, + docTypes: NewDocTypeSet(NewsDoc), + paginator: newStashPaginator(c), }, filterSection: { key: filterSection, - paginator: newStashPaginator(), + docTypes: DocTypeSet{}, + paginator: newStashPaginator(c), }, } } -// String returns a styled version of the status message appropriate for the +// Render returns a styled version of the status message appropriate for the // given context. -func (s statusMessage) String() string { - switch s.status { //nolint:exhaustive +func (s statusMessage) Render(st *styles) string { + switch s.status { case subtleStatusMessage: - return dimGreenFg(s.message) + return st.DimGreenFg(s.message) case errorStatusMessage: - return redFg(s.message) + return st.RedFg(s.message) default: - return greenFg(s.message) + return st.GreenFg(s.message) } } @@ -140,9 +155,12 @@ 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 @@ -155,8 +173,8 @@ type stashModel struct { // Index of the section we're currently looking at sectionIndex int - // Tracks if docs were loaded - loaded bool + // Tracks what exactly is loaded between the stash, news and local files + loaded DocTypeSet // The master set of markdown documents we're working with. markdowns []*markdown @@ -170,11 +188,19 @@ 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 int64 + serverPage int +} + +func (m stashModel) localOnly() bool { + return m.common.cfg.localOnly() +} + +func (m stashModel) stashedOnly() bool { + return m.common.cfg.stashedOnly() } func (m stashModel) loadingDone() bool { - return m.loaded + return m.loaded.Equals(m.common.cfg.DocumentTypes.Difference(ConvertedDoc)) } func (m stashModel) currentSection() *section { @@ -197,30 +223,51 @@ 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 || openingDocument + return loading || stashing || openingDocument } func (m *stashModel) setSize(width, height int) { m.common.width = width m.common.height = height - m.filterInput.Width = width - stashViewHorizontalPadding*2 - ansi.PrintableRuneWidth( - m.filterInput.Prompt, - ) + m.noteInput.Width = width - stashViewHorizontalPadding*2 - ansi.PrintableRuneWidth(m.noteInput.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 - sortMarkdowns(m.markdowns) + // 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)) // 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. @@ -247,7 +294,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() + return m.filterApplied() && m.selectionState != selectionSettingNote } // Update pagination according to the amount of markdowns for the current @@ -293,16 +340,58 @@ func (m stashModel) selectedMarkdown() *markdown { // Adds markdown documents to the model. func (m *stashModel) addMarkdowns(mds ...*markdown) { - if len(mds) == 0 { + 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 { return } - m.markdowns = append(m.markdowns, mds...) - if !m.filterApplied() { - sortMarkdowns(m.markdowns) + var mds []*markdown + if m.filterState == filtering { + mds = m.getVisibleMarkdowns() + } else { + mds = m.markdowns } - m.updatePagination() + 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 } // Returns the markdowns that should be currently shown. @@ -311,17 +400,47 @@ func (m stashModel) getVisibleMarkdowns() []*markdown { return m.filteredMarkdowns } - return m.markdowns + 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 } // 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 - cmd := loadLocalMarkdown(md) + + if md.docType == LocalDoc { + cmd = loadLocalMarkdown(md) + } else { + cmd = loadRemoteMarkdown(m.common.cc, 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{} @@ -374,64 +493,169 @@ func (m *stashModel) moveCursorDown() { // INIT func newStashModel(common *commonModel) stashModel { - sp := spinner.New() + sp := spinner.New(common.ctx) sp.Spinner = spinner.Line - sp.Style = stashSpinnerStyle + sp.Style = common.styles.StashSpinnerStyle - si := textinput.New() + ni := textinput.New(common.ctx) + ni.Prompt = "Memo:" + ni.PromptStyle = common.styles.StashInputPromptStyle + ni.CursorStyle = common.styles.StashInputCursorStyle + ni.CharLimit = noteCharacterLimit + ni.Focus() + + si := textinput.New(common.ctx) si.Prompt = "Find:" - si.PromptStyle = stashInputPromptStyle - si.Cursor.Style = stashInputCursorStyle + si.PromptStyle = common.styles.StashInputPromptStyle + si.CursorStyle = common.styles.StashInputCursorStyle + si.CharLimit = noteCharacterLimit si.Focus() - s := []section{ - sections[documentsSection], + 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], + } } m := stashModel{ common: common, spinner: sp, + noteInput: ni, filterInput: si, serverPage: 1, + loaded: NewDocTypeSet(), sections: s, } return m } -func newStashPaginator() paginator.Model { - p := paginator.New() +func newStashPaginator(common *commonModel) paginator.Model { + p := paginator.New(common.ctx) p.Type = paginator.Dots - p.ActiveDot = brightGrayFg("•") - p.InactiveDot = darkGrayFg.Render("•") + p.ActiveDot = common.styles.BrightGrayFg("•") + p.InactiveDot = common.styles.DarkGrayFg("•") return p } // UPDATE -func (m stashModel) update(msg tea.Msg) (stashModel, tea.Cmd) { +func (m stashModel) update(ctx tea.Context, msg tea.Msg) (stashModel, tea.Cmd) { var cmds []tea.Cmd switch msg := msg.(type) { 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 = true + 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 case filteredMarkdownMsg: m.filteredMarkdowns = msg - m.setCursor(0) return m, nil case spinner.TickMsg: if m.shouldSpin() { var cmd tea.Cmd - m.spinner, cmd = m.spinner.Update(msg) + m.spinner, cmd = m.spinner.Update(ctx, msg) 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() @@ -443,8 +667,17 @@ 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 { //nolint:exhaustive + switch m.viewState { case stashStateReady: cmds = append(cmds, m.handleDocumentBrowsing(msg)) case stashStateShowingError: @@ -511,20 +744,17 @@ 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() - - // In case no file is available - if md == nil { - return nil + if md == nil || md.docType != LocalDoc { + break } - - return openEditor(md.localPath, 0) + file := m.selectedMarkdown().localPath + if file == "" { + break + } + return openEditor(file) // Open document case keyEnter: @@ -542,13 +772,14 @@ 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.markdowns + m.filteredMarkdowns = m.getFilterableMarkdowns() m.paginator().Page = 0 m.setCursor(0) @@ -557,6 +788,99 @@ 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 @@ -574,7 +898,7 @@ func (m *stashModel) handleDocumentBrowsing(msg tea.Msg) tea.Cmd { // Update paginator. Pagination key handling is done here, but it could // also be moved up to this level, in which case we'd use model methods // like model.PageUp(). - newPaginatorModel, cmd := m.paginator().Update(msg) + newPaginatorModel, cmd := m.paginator().Update(m.common.ctx, msg) m.setPaginator(newPaginatorModel) cmds = append(cmds, cmd) @@ -597,12 +921,90 @@ 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 { //nolint:nestif + if msg, ok := msg.(tea.KeyMsg); ok { switch msg.String() { case keyEsc: // Cancel filtering @@ -648,7 +1050,7 @@ func (m *stashModel) handleFiltering(msg tea.Msg) tea.Cmd { } // Update the filter text input component - newFilterInputModel, inputCmd := m.filterInput.Update(msg) + newFilterInputModel, inputCmd := m.filterInput.Update(m.common.ctx, msg) currentFilterVal := m.filterInput.Value() newFilterVal := newFilterInputModel.Value() m.filterInput = newFilterInputModel @@ -665,38 +1067,96 @@ 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(m.common.ctx, msg) + m.noteInput = newNoteInputModel + cmds = append(cmds, noteInputCmd) + + return tea.Batch(cmds...) +} + // VIEW -func (m stashModel) view() string { +func (m stashModel) view(ctx tea.Context) string { var s string switch m.viewState { case stashStateShowingError: - return errorView(m.err, false) + return errorView(&m.common.styles, m.err, false) case stashStateLoadingDocument: - s += " " + m.spinner.View() + " Loading document..." + s += " " + m.spinner.View(ctx) + " Loading document..." case stashStateReady: + loadingIndicator := " " if m.shouldSpin() { - loadingIndicator = m.spinner.View() + loadingIndicator = m.spinner.View(ctx) + } + + var header string + switch m.selectionState { + case selectionPromptingDelete: + header = m.common.styles.RedFg("Delete this item from your stash? ") + m.common.styles.FaintRedFg("(y/N)") + case selectionSettingNote: + header = m.common.styles.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). - header := m.headerView() + if header == "" { + header = m.headerView() + } // Rules for the logo, filter and status message. logoOrFilter := " " if m.showStatusMessage && m.filterState == filtering { - logoOrFilter += m.statusMessage.String() + logoOrFilter += m.statusMessage.Render(&m.common.styles) } else if m.filterState == filtering { - logoOrFilter += m.filterInput.View() + logoOrFilter += m.filterInput.View(ctx) } else { - logoOrFilter += glowLogoView() + logoOrFilter += m.common.styles.LogoStyle.Render(" Glow ") if m.showStatusMessage { - logoOrFilter += " " + m.statusMessage.String() + logoOrFilter += " " + m.statusMessage.Render(&m.common.styles) } } - logoOrFilter = truncate.StringWithTail(logoOrFilter, uint(m.common.width-1), ellipsis) //nolint:gosec + logoOrFilter = truncate.StringWithTail(logoOrFilter, uint(m.common.width-1), ellipsis) help, helpHeight := m.helpView() @@ -714,7 +1174,7 @@ func (m stashModel) view() string { var pagination string if m.paginator().TotalPages > 1 { - pagination = m.paginator().View() + pagination = m.paginator().View(ctx) // If the dot pagination is wider than the width of the window // use the arabic paginator. @@ -727,10 +1187,14 @@ 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. - p := *(m.paginator()) + var p paginator.Model = *(m.paginator()) p.Type = paginator.Arabic - pagination = paginationStyle.Render(p.View()) + pagination = m.common.styles.PaginationStyle.Render(p.View(ctx)) } + + // 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( @@ -747,29 +1211,42 @@ func (m stashModel) view() string { return "\n" + indent(s, stashIndent) } -func glowLogoView() string { - return logoStyle.Render(" Glow ") -} - func (m stashModel) headerView() string { - localCount := len(m.markdowns) + localCount := m.countMarkdowns(LocalDoc) + stashedCount := m.countMarkdowns(StashedDoc) + m.countMarkdowns(ConvertedDoc) + newsCount := m.countMarkdowns(NewsDoc) var sections []string //nolint:prealloc // Filter results if m.filterState == filtering { - if localCount == 0 { - return grayFg("Nothing found.") + if localCount+stashedCount+newsCount == 0 { + return m.common.styles.GrayFg("Nothing found.") } if localCount > 0 { sections = append(sections, fmt.Sprintf("%d local", localCount)) } - - for i := range sections { - sections[i] = grayFg(sections[i]) + if stashedCount > 0 { + sections = append(sections, fmt.Sprintf("%d stashed", stashedCount)) } - return strings.Join(sections, dividerDot.String()) + for i := range sections { + sections[i] = m.common.styles.GrayFg(sections[i]) + } + + return strings.Join(sections, m.common.styles.DividerDot) + } + + if m.loadingDone() && len(m.markdowns) == 0 { + var maybeOffline string + if m.common.authStatus == authFailed { + maybeOffline = " " + m.common.styles.OfflineHeaderNote + } + + if m.stashedOnly() { + return m.common.styles.SubtleStyle.Render("Can’t load stash") + maybeOffline + } + return m.common.styles.SubtleStyle.Render("No markdown files found") + maybeOffline } // Tabs @@ -777,22 +1254,39 @@ func (m stashModel) headerView() string { var s string switch v.key { - case documentsSection: - s = fmt.Sprintf("%d documents", localCount) - + 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 filterSection: s = fmt.Sprintf("%d “%s”", len(m.filteredMarkdowns), m.filterInput.Value()) } if m.sectionIndex == i && len(m.sections) > 1 { - s = selectedTabStyle.Render(s) + s = m.common.styles.SelectedTabStyle.Render(s) } else { - s = tabStyle.Render(s) + s = m.common.styles.TabStyle.Render(s) } sections = append(sections, s) } - return strings.Join(sections, dividerBar.String()) + s := strings.Join(sections, m.common.styles.DividerBar) + if m.common.authStatus == authFailed { + s += m.common.styles.DividerDot + m.common.styles.OfflineHeaderNote + } + + return s } func (m stashModel) populatedView() string { @@ -803,16 +1297,32 @@ func (m stashModel) populatedView() string { // Empty states if len(mds) == 0 { f := func(s string) { - b.WriteString(" " + grayFg(s)) + b.WriteString(" " + m.common.styles.GrayFg(s)) } switch m.sections[m.sectionIndex].key { - case documentsSection: + case localSection: if m.loadingDone() { - f("No files found.") + f("No local 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 "" } @@ -849,15 +1359,39 @@ 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 { - log.Debug("error reading local file", "error", err) + if debug { + log.Println("error reading local markdown:", err) + } return errMsg{err} } md.Body = string(data) @@ -865,14 +1399,27 @@ 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.markdowns) // return everything + return filteredMarkdownMsg(m.getFilterableMarkdowns()) // return everything } targets := []string{} - mds := m.markdowns + mds := m.getFilterableMarkdowns() for _, t := range mds { targets = append(targets, t.filterValue) @@ -889,3 +1436,55 @@ 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 6c0b474..92f8b19 100644 --- a/ui/stashhelp.go +++ b/ui/stashhelp.go @@ -30,7 +30,7 @@ func newHelpColumn(pairs ...string) (h helpColumn) { } // render returns styled and formatted rows from keys and values. -func (h helpColumn) render(height int) (rows []string) { +func (h helpColumn) render(st *styles, height int) (rows []string) { keyWidth, valWidth := h.maxWidths() for i := 0; i < height; i++ { @@ -44,11 +44,11 @@ func (h helpColumn) render(height int) (rows []string) { switch k { case "s": - k = greenFg(k) - v = semiDimGreenFg(v) + k = st.GreenFg(k) + v = st.SemiDimGreenFg(v) default: - k = grayFg(k) - v = midGrayFg(v) + k = st.GrayFg(k) + v = st.MidGrayFg(v) } } b.WriteString(k) @@ -99,7 +99,18 @@ 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 @@ -108,6 +119,13 @@ 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"} } @@ -125,23 +143,27 @@ func (m stashModel) helpView() (string, int) { } // If we're browsing a filtered set - if m.filterApplied() { - filterHelp = []string{"/", "edit search", "esc", "clear filter"} + if m.filterState == filterApplied { + filterHelp = []string{"/", "edit search", "esc", "clear search"} } 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 @@ -159,15 +181,13 @@ 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, max(numLines, minHelpViewHeight) + return str, numLines } return m.miniHelpView(concatStringSlices(groups...)...), 1 } @@ -176,12 +196,13 @@ func (m stashModel) renderHelp(groups ...[]string) (string, int) { // 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 { + st := &m.common.styles if len(entries) == 0 { return "" } var ( - truncationChar = subtleStyle.Render("…") + truncationChar = st.SubtleStyle.Render("…") truncationWidth = ansi.PrintableRuneWidth(truncationChar) ) @@ -199,13 +220,19 @@ func (m stashModel) miniHelpView(entries ...string) string { k := entries[i] v := entries[i+1] - k = grayFg(k) - v = midGrayFg(v) + switch k { + case "s": + k = st.GreenFg(k) + v = st.SemiDimGreenFg(v) + default: + k = st.GrayFg(k) + v = st.MidGrayFg(v) + } next = fmt.Sprintf("%s %s", k, v) if i < len(entries)-2 { - next += dividerDot.String() + next += st.DividerDot } // Only this (and the following) help text items if we have the @@ -243,7 +270,7 @@ func (m stashModel) fullHelpView(groups ...[]string) string { // Build columns for _, c := range columns { - renderedCols = append(renderedCols, c.render(tallestCol)) + renderedCols = append(renderedCols, c.render(&m.common.styles, tallestCol)) } // Merge columns diff --git a/ui/stashitem.go b/ui/stashitem.go index 0000928..8e50498 100644 --- a/ui/stashitem.go +++ b/ui/stashitem.go @@ -2,99 +2,138 @@ package ui import ( "fmt" + "log" "strings" "github.com/charmbracelet/lipgloss" - "github.com/charmbracelet/log" + "github.com/muesli/reflow/ansi" "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) //nolint:gosec - gutter string - title = truncate.StringWithTail(md.Note, truncateTo, ellipsis) - date = md.relativeTime() - editedBy = "" - hasEditedBy = false - icon = "" - separator = "" + truncateTo = uint(m.common.width - stashViewHorizontalPadding*2) + gutter string + title = md.Note + date = md.relativeTime() + icon = "" ) + 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 + styles := &m.common.styles // 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 { //nolint:nestif + if isSelected && !isFiltering || singleFilteredItem { // Selected item - if m.statusMessage == stashingStatusMessage { - gutter = greenFg(verticalLine) - icon = dimGreenFg(icon) - title = greenFg(title) - date = semiDimGreenFg(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)) + + switch m.selectionState { + case selectionPromptingDelete: + gutter = styles.FaintRedFg(verticalLine) + icon = styles.FaintRedFg(icon) + title = styles.RedFg(title) + date = styles.FaintRedFg(date) + case selectionSettingNote: + gutter = styles.DullYellowFg(verticalLine) + icon = "" + title = m.noteInput.View(m.common.ctx) + date = styles.DullYellowFg(date) + default: + if m.common.latestFileStashed == md.stashID && + m.statusMessage == stashingStatusMessage { + gutter = styles.GreenFg(verticalLine) + icon = styles.DimGreenFg(icon) + title = styles.GreenFg(title) + date = styles.SemiDimGreenFg(date) } else { - title = fuchsiaFg(title) - icon = fuchsiaFg(icon) + gutter = styles.DullFuchsiaFg(verticalLine) + icon = styles.DullFuchsiaFg(icon) + if m.currentSection().key == filterSection && + m.filterState == filterApplied || singleFilteredItem { + s := lipgloss.NewStyle().Foreground(fuchsia) + title = styleFilteredText(title, m.filterInput.Value(), s, s.Copy().Underline(true)) + } else { + title = styles.FuchsiaFg(title) + } + date = styles.DullFuchsiaFg(date) } - 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) - title = dimNormalFg(title) - date = dimBrightGrayFg(date) - editedBy = dimBrightGrayFg(editedBy) - separator = dimBrightGrayFg(separator) - } else { - icon = greenFg(icon) + // Regular (non-selected) items - 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) + gutter = " " + + if m.common.latestFileStashed == md.stashID && + m.statusMessage == stashingStatusMessage { + icon = styles.DimGreenFg(icon) + title = styles.GreenFg(title) + date = styles.SemiDimGreenFg(date) + } else if md.docType == NewsDoc { + if isFiltering && m.filterInput.Value() == "" { + title = styles.DimIndigoFg(title) + date = styles.DimSubtleIndigoFg(date) + } else { + s := lipgloss.NewStyle().Foreground(indigo) + title = styleFilteredText(title, m.filterInput.Value(), s, s.Copy().Underline(true)) + date = styles.SubtleIndigoFg(date) + } + } else if isFiltering && m.filterInput.Value() == "" { + icon = styles.DimGreenFg(icon) + if title == noMemoTitle { + title = styles.DimBrightGrayFg(title) + } else { + title = styles.DimNormalFg(title) + } + date = styles.DimBrightGrayFg(date) + } else { + icon = styles.GreenFg(icon) + if title == noMemoTitle { + title = styles.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 = styles.BrightGrayFg(date) } } - fmt.Fprintf(b, "%s %s%s%s%s\n", gutter, icon, separator, separator, title) + fmt.Fprintf(b, "%s %s%s\n", gutter, icon, 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 { - log.Error("error normalizing", "haystack", haystack, "error", err) + if err != nil && debug { + log.Printf("error normalizing '%s': %v", haystack, err) } matches := fuzzy.Find(needles, []string{normalizedHay}) diff --git a/ui/styles.go b/ui/styles.go index fd25882..f4a759e 100644 --- a/ui/styles.go +++ b/ui/styles.go @@ -1,46 +1,158 @@ package ui -import "github.com/charmbracelet/lipgloss" +import ( + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + . "github.com/charmbracelet/lipgloss" +) //nolint: revive // Colors. var ( - 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"} + 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"} + fuchsia = 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"} ) +type styler func(...string) string + // Ulimately, we'll transition to named styles. -var ( - 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 -) +// nolint:deadcode,unused,varcheck +type styles struct { + NormalFg styler + DimNormalFg styler + + BrightGrayFg styler + DimBrightGrayFg styler + + GrayFg styler + MidGrayFg styler + DarkGrayFg styler + + GreenFg styler + SemiDimGreenFg styler + DimGreenFg styler + + FuchsiaFg styler + DimFuchsiaFg styler + + DullFuchsiaFg styler + DimDullFuchsiaFg styler + + IndigoFg styler + DimIndigoFg styler + + SubtleIndigoFg styler + DimSubtleIndigoFg styler + + YellowFg styler + DullYellowFg styler + RedFg styler + FaintRedFg styler + + TabStyle Style + SelectedTabStyle Style + ErrorTitleStyle Style + SubtleStyle Style + PaginationStyle Style + + LogoStyle Style + StashSpinnerStyle Style + StashInputPromptStyle Style + StashInputCursorStyle Style + + DividerDot string + DividerBar string + OfflineHeaderNote string +} + +func defaultStyles(ctx tea.Context) styles { + darkGrayFg := ctx.NewStyle().Foreground(darkGray).Render + subtleStyle := ctx.NewStyle(). + Foreground(AdaptiveColor{Light: "#9B9B9B", Dark: "#5C5C5C"}) + + return styles{ + NormalFg: ctx.NewStyle().Foreground(normal).Render, + DimNormalFg: ctx.NewStyle().Foreground(normalDim).Render, + + BrightGrayFg: ctx.NewStyle().Foreground(brightGray).Render, + DimBrightGrayFg: ctx.NewStyle().Foreground(dimBrightGray).Render, + + GrayFg: ctx.NewStyle().Foreground(gray).Render, + MidGrayFg: ctx.NewStyle().Foreground(midGray).Render, + DarkGrayFg: darkGrayFg, + + GreenFg: ctx.NewStyle().Foreground(green).Render, + SemiDimGreenFg: ctx.NewStyle().Foreground(semiDimGreen).Render, + DimGreenFg: ctx.NewStyle().Foreground(dimGreen).Render, + + FuchsiaFg: ctx.NewStyle().Foreground(fuchsia).Render, + DimFuchsiaFg: ctx.NewStyle().Foreground(dimFuchsia).Render, + + DullFuchsiaFg: ctx.NewStyle().Foreground(dullFuchsia).Render, + DimDullFuchsiaFg: ctx.NewStyle().Foreground(dimDullFuchsia).Render, + + IndigoFg: ctx.NewStyle().Foreground(fuchsia).Render, + DimIndigoFg: ctx.NewStyle().Foreground(dimIndigo).Render, + + SubtleIndigoFg: ctx.NewStyle().Foreground(subtleIndigo).Render, + DimSubtleIndigoFg: ctx.NewStyle().Foreground(dimSubtleIndigo).Render, + + YellowFg: ctx.NewStyle().Foreground(yellowGreen).Render, // renders light green on light backgrounds + DullYellowFg: ctx.NewStyle().Foreground(dullYellowGreen).Render, // renders light green on light backgrounds + RedFg: ctx.NewStyle().Foreground(red).Render, + FaintRedFg: ctx.NewStyle().Foreground(faintRed).Render, + TabStyle: ctx.NewStyle(). + Foreground(AdaptiveColor{Light: "#909090", Dark: "#626262"}), + + SelectedTabStyle: ctx.NewStyle(). + Foreground(AdaptiveColor{Light: "#333333", Dark: "#979797"}), + + ErrorTitleStyle: ctx.NewStyle(). + Foreground(cream). + Background(red). + Padding(0, 1), + + SubtleStyle: subtleStyle, + + PaginationStyle: subtleStyle.Copy(), + + LogoStyle: ctx.NewStyle(). + Foreground(lipgloss.Color("#ECFD65")). + Background(fuchsia). + Bold(true), + + StashSpinnerStyle: ctx.NewStyle(). + Foreground(gray), + StashInputPromptStyle: ctx.NewStyle(). + Foreground(yellowGreen). + MarginRight(1), + StashInputCursorStyle: ctx.NewStyle(). + Foreground(fuchsia). + MarginRight(1), + + DividerDot: darkGrayFg(" • "), + DividerBar: darkGrayFg(" │ "), + OfflineHeaderNote: darkGrayFg("(Offline)"), + } +} diff --git a/ui/ui.go b/ui/ui.go index 3537d3f..724ccb0 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -1,24 +1,32 @@ -// 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/glamour/styles" - "github.com/charmbracelet/glow/v2/utils" - "github.com/charmbracelet/log" + "github.com/charmbracelet/charm" + "github.com/charmbracelet/charm/keygen" + "github.com/charmbracelet/glow/utils" "github.com/muesli/gitcha" - te "github.com/muesli/termenv" + "github.com/segmentio/ksuid" ) const ( - statusMessageTimeout = time.Second * 3 // how long to show status messages like "stashed!" + noteCharacterLimit = 256 // should match server + statusMessageTimeout = time.Second * 2 // 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 ( @@ -27,25 +35,29 @@ 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, content string) *tea.Program { - log.Debug( - "Starting glow", - "high_perf_pager", - cfg.HighPerformancePager, - "glamour", - cfg.GlamourEnabled, - ) - +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 + } config = cfg opts := []tea.ProgramOption{tea.WithAltScreen()} if cfg.EnableMouse { opts = append(opts, tea.WithMouseCellMotion()) } - m := newModel(cfg, content) - return tea.NewProgram(m, opts...) + return tea.NewProgram(newModel(cfg), opts...) } type errMsg struct{ err error } @@ -53,6 +65,10 @@ 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 @@ -62,7 +78,16 @@ 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 applies @@ -89,18 +114,63 @@ 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 - cwd string - width int - height int + ctx tea.Context + 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{} + + styles styles +} + +func (c commonModel) isStashing() bool { + return len(c.filesStashing) > 0 } type model struct { - common *commonModel - state state - fatalErr error + common *commonModel + state state + keygenState keygenState + fatalErr error // Sub-models stash stashModel @@ -121,7 +191,7 @@ func (m *model) unloadDocument() []tea.Cmd { var batch []tea.Cmd if m.pager.viewport.HighPerformanceRendering { - batch = append(batch, tea.ClearScrollArea) //nolint:staticcheck + batch = append(batch, tea.ClearScrollArea) } if !m.stash.shouldSpin() { @@ -130,79 +200,62 @@ func (m *model) unloadDocument() []tea.Cmd { return batch } -func newModel(cfg Config, content string) tea.Model { - initSections() - - if cfg.GlamourStyle == styles.AutoStyle { - if te.HasDarkBackground() { - cfg.GlamourStyle = styles.DarkStyle - } else { - cfg.GlamourStyle = styles.LightStyle - } +func newModel(cfg Config) tea.Model { + if len(cfg.DocumentTypes) == 0 { + cfg.DocumentTypes.Add(LocalDoc, StashedDoc, ConvertedDoc, NewsDoc) } common := commonModel{ - cfg: cfg, + cfg: cfg, + authStatus: authConnecting, + filesStashed: make(map[ksuid.KSUID]struct{}), + filesStashing: make(map[ksuid.KSUID]struct{}), } - m := model{ - common: &common, - state: stateShowStash, - pager: newPagerModel(&common), - stash: newStashModel(&common), + return model{ + common: &common, + state: stateShowStash, + keygenState: keygenUnstarted, } +} - 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(), +func (m model) Init(ctx tea.Context) (tea.Model, tea.Cmd) { + if m.common.cfg.GlamourStyle == "auto" { + if ctx.HasLightBackground() { + m.common.cfg.GlamourStyle = "light" + } else { + m.common.cfg.GlamourStyle = "dark" } } - return m -} + // Initialize styles & sub-models + m.common.ctx = ctx + m.common.styles = defaultStyles(ctx) -func (m model) Init() tea.Cmd { - cmds := []tea.Cmd{m.stash.spinner.Tick} + // Initialize sections after we have the styles + initSections(m.common) - 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)) + m.pager = newPagerModel(m.common) + m.stash = newStashModel(m.common) + + var cmds []tea.Cmd + d := m.common.cfg.DocumentTypes + + if d.Contains(StashedDoc) || d.Contains(NewsDoc) { + cmds = append(cmds, + newCharmClient, + m.stash.spinner.Tick, + ) } - return tea.Batch(cmds...) + if d.Contains(LocalDoc) { + cmds = append(cmds, findLocalFiles(m)) + } + + return m, tea.Batch(cmds...) } -func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m model) Update(ctx tea.Context, msg tea.Msg) (tea.Model, tea.Cmd) { // If there's been an error, any key exits if m.fatalErr != nil { if _, ok := msg.(tea.KeyMsg); ok { @@ -216,45 +269,43 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyMsg: switch msg.String() { case "esc": - if m.state == stateShowDocument || m.stash.viewState == stashStateLoadingDocument { + if m.state == stateShowDocument { 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 { //nolint:exhaustive + switch m.state { case stateShowStash: // pass through all keys if we're editing the filter - if m.stash.filterState == filtering { - m.stash, cmd = m.stash.update(msg) + if m.stash.filterState == filtering || m.stash.selectionState == selectionSettingNote { + m.stash, cmd = m.stash.update(ctx, 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(ctx, 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 { + if m.state == stateShowDocument && m.pager.state != pagerStateSetNote { 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 @@ -272,20 +323,70 @@ 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 - body := string(utils.RemoveFrontmatter([]byte(msg.Body))) - cmds = append(cmds, renderWithGlamour(m.pager, body)) + msg.Body = string(utils.RemoveFrontmatter([]byte(msg.Body))) + cmds = append(cmds, renderWithGlamour(m.pager, msg.Body)) case contentRenderedMsg: m.state = stateShowDocument - case localFileSearchFinished: + 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(ctx, msg) + m.stash = stashModel + return m, cmd + + case localFileSearchFinished, gotStashMsg, gotNewsMsg: // 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. - stashModel, cmd := m.stash.update(msg) + stashModel, cmd := m.stash.update(ctx, msg) m.stash = stashModel return m, cmd @@ -300,9 +401,38 @@ 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) + newStashModel, cmd := m.stash.update(ctx, msg) m.stash = newStashModel cmds = append(cmds, cmd) } @@ -311,12 +441,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Process children switch m.state { case stateShowStash: - newStashModel, cmd := m.stash.update(msg) + newStashModel, cmd := m.stash.update(ctx, msg) m.stash = newStashModel cmds = append(cmds, cmd) case stateShowDocument: - newPagerModel, cmd := m.pager.update(msg) + newPagerModel, cmd := m.pager.update(ctx, msg) m.pager = newPagerModel cmds = append(cmds, cmd) } @@ -324,20 +454,20 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Batch(cmds...) } -func (m model) View() string { +func (m model) View(ctx tea.Context) string { if m.fatalErr != nil { - return errorView(m.fatalErr, true) + return errorView(&m.common.styles, m.fatalErr, true) } - switch m.state { //nolint:exhaustive + switch m.state { case stateShowDocument: - return m.pager.View() + return m.pager.View(ctx) default: - return m.stash.view() + return m.stash.view(ctx) } } -func errorView(err error, fatal bool) string { +func errorView(st *styles, err error, fatal bool) string { exitMsg := "press any key to " if fatal { exitMsg += "exit" @@ -345,20 +475,19 @@ func errorView(err error, fatal bool) string { exitMsg += "return" } s := fmt.Sprintf("%s\n\n%v\n\n%s", - errorTitleStyle.Render("ERROR"), + st.ErrorTitleStyle.Render("ERROR"), err, - subtleStyle.Render(exitMsg), + st.SubtleStyle.Render(exitMsg), ) return "\n" + indent(s, 3) } // COMMANDS -func findLocalFiles(m commonModel) tea.Cmd { +func findLocalFiles(m model) tea.Cmd { return func() tea.Msg { - log.Info("findLocalFiles") var ( - cwd = m.cfg.Path + cwd = m.common.cfg.WorkingDirectory err error ) @@ -374,22 +503,26 @@ func findLocalFiles(m commonModel) tea.Cmd { // Note that this is one error check for both cases above if err != nil { - log.Error("error finding local files", "error", err) + if debug { + log.Println("error finding local files:", err) + } return errMsg{err} } - 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)) + if debug { + log.Println("local directory is:", cwd) } + var ignore []string + if !m.common.cfg.ShowAllFiles { + ignore = ignorePatterns(m) + } + + ch, err := gitcha.FindFilesExcept(cwd, markdownExtensions, ignore) if err != nil { - log.Error("error finding local files", "error", err) + if debug { + log.Println("error finding local files:", err) + } return errMsg{err} } @@ -406,11 +539,186 @@ func findNextLocalFile(m model) tea.Cmd { return foundLocalFileMsg(res) } // We're done - log.Debug("local file search finished") + if debug { + log.Println("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 successfully") + } + 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 @@ -424,17 +732,20 @@ 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 { - return &markdown{ + md := &markdown{ + docType: LocalDoc, localPath: res.Path, - Note: stripAbsolutePath(res.Path, cwd), - Modtime: res.Info.ModTime(), + Markdown: charm.Markdown{ + Note: stripAbsolutePath(res.Path, cwd), + CreatedAt: res.Info.ModTime(), + }, } + + return md } func stripAbsolutePath(fullPath, cwd string) string { - fp, _ := filepath.EvalSymlinks(fullPath) - cp, _ := filepath.EvalSymlinks(cwd) - return strings.ReplaceAll(fp, cp+string(os.PathSeparator), "") + return strings.Replace(fullPath, cwd+string(os.PathSeparator), "", -1) } // Lightweight version of reflow's indent function. @@ -450,3 +761,17 @@ 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 deleted file mode 100644 index f716681..0000000 --- a/url.go +++ /dev/null @@ -1,86 +0,0 @@ -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 deleted file mode 100644 index 9683892..0000000 --- a/url_test.go +++ /dev/null @@ -1,30 +0,0 @@ -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 9f33a1e..fe38c13 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -1,16 +1,9 @@ -// 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" ) @@ -31,7 +24,7 @@ func detectFrontmatter(c []byte) []int { return []int{-1, -1} } -// ExpandPath expands tilde and all environment variables from the given path. +// Expands tilde and all environment variables from the given path. func ExpandPath(path string) string { s, err := homedir.Expand(path) if err == nil { @@ -39,75 +32,3 @@ 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) -}