From c0e09f76c8086131706af842fcb112e3d28510cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Duffeck?= Date: Mon, 23 Jun 2025 11:24:01 +0200 Subject: [PATCH] Add a "posixfs consistency" command This command checks posixfs storages for inconsistencies and fixes them. --- go.mod | 1 + go.sum | 2 + opencloud/pkg/command/posixfs.go | 376 ++++++ vendor/github.com/theckman/yacspin/.gitignore | 25 + .../theckman/yacspin/.golangci.yaml | 71 + vendor/github.com/theckman/yacspin/LICENSE | 201 +++ vendor/github.com/theckman/yacspin/README.md | 274 ++++ .../theckman/yacspin/character_sets.go | 121 ++ vendor/github.com/theckman/yacspin/colors.go | 174 +++ vendor/github.com/theckman/yacspin/spinner.go | 1191 +++++++++++++++++ vendor/modules.txt | 3 + 11 files changed, 2439 insertions(+) create mode 100644 opencloud/pkg/command/posixfs.go create mode 100644 vendor/github.com/theckman/yacspin/.gitignore create mode 100644 vendor/github.com/theckman/yacspin/.golangci.yaml create mode 100644 vendor/github.com/theckman/yacspin/LICENSE create mode 100644 vendor/github.com/theckman/yacspin/README.md create mode 100644 vendor/github.com/theckman/yacspin/character_sets.go create mode 100644 vendor/github.com/theckman/yacspin/colors.go create mode 100644 vendor/github.com/theckman/yacspin/spinner.go diff --git a/go.mod b/go.mod index bb221e32f..3190dbf62 100644 --- a/go.mod +++ b/go.mod @@ -79,6 +79,7 @@ require ( github.com/spf13/cobra v1.9.1 github.com/stretchr/testify v1.10.0 github.com/test-go/testify v1.1.4 + github.com/theckman/yacspin v0.13.12 github.com/thejerf/suture/v4 v4.0.6 github.com/tidwall/gjson v1.18.0 github.com/tus/tusd/v2 v2.8.0 diff --git a/go.sum b/go.sum index c1de30252..bef3d4fc6 100644 --- a/go.sum +++ b/go.sum @@ -1083,6 +1083,8 @@ github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU= github.com/thanhpk/randstr v1.0.6 h1:psAOktJFD4vV9NEVb3qkhRSMvYh4ORRaj1+w/hn4B+o= github.com/thanhpk/randstr v1.0.6/go.mod h1:M/H2P1eNLZzlDwAzpkkkUvoyNNMbzRGhESZuEQk3r0U= +github.com/theckman/yacspin v0.13.12 h1:CdZ57+n0U6JMuh2xqjnjRq5Haj6v1ner2djtLQRzJr4= +github.com/theckman/yacspin v0.13.12/go.mod h1:Rd2+oG2LmQi5f3zC3yeZAOl245z8QOvrH4OPOJNZxLg= github.com/thejerf/suture/v4 v4.0.6 h1:QsuCEsCqb03xF9tPAsWAj8QOAJBgQI1c0VqJNaingg8= github.com/thejerf/suture/v4 v4.0.6/go.mod h1:gu9Y4dXNUWFrByqRt30Rm9/UZ0wzRSt9AJS6xu/ZGxU= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= diff --git a/opencloud/pkg/command/posixfs.go b/opencloud/pkg/command/posixfs.go new file mode 100644 index 000000000..8ed5ce80b --- /dev/null +++ b/opencloud/pkg/command/posixfs.go @@ -0,0 +1,376 @@ +package command + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/opencloud-eu/opencloud/opencloud/pkg/register" + "github.com/opencloud-eu/opencloud/pkg/config" + "github.com/pkg/xattr" + "github.com/theckman/yacspin" + "github.com/urfave/cli/v2" + "github.com/vmihailenco/msgpack/v5" +) + +// Define the names of the extended attributes we are working with. +const ( + parentIDAttrName = "user.oc.parentid" + idAttrName = "user.oc.id" + spaceIDAttrName = "user.oc.space.id" + ownerIDAttrName = "user.oc.owner.id" +) + +var ( + spinner *yacspin.Spinner + restartRequired = false +) + +// EntryInfo holds information about a directory entry. +type EntryInfo struct { + Path string + ModTime time.Time + ParentID string +} + +// PosixfsCommand is the entrypoint for the groups command. +func PosixfsCommand(cfg *config.Config) *cli.Command { + return &cli.Command{ + Name: "posixfs", + Usage: `cli tools to inspect and manipulate a posixfs storage.`, + Category: "maintenance", + Subcommands: []*cli.Command{ + consistencyCmd(cfg), + }, + } +} + +func init() { + register.AddCommand(PosixfsCommand) +} + +// consistencyCmd returns a command to check the consistency of the posixfs storage. +func consistencyCmd(cfg *config.Config) *cli.Command { + return &cli.Command{ + Name: "consistency", + Usage: "check the consistency of the posixfs storage", + Action: func(c *cli.Context) error { + return checkPosixfsConsistency(c, cfg) + }, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "root", + Aliases: []string{"r"}, + Required: true, + Usage: "Path to the root directory of the posixfs storage", + }, + }, + } +} + +// checkPosixfsConsistency checks the consistency of the posixfs storage. +func checkPosixfsConsistency(c *cli.Context, cfg *config.Config) error { + rootPath := c.String("root") + indexesPath := filepath.Join(rootPath, "indexes") + + _, err := os.Stat(indexesPath) + if err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("consistency check failed: '%s' is not a posixfs root", rootPath) + } + return fmt.Errorf("error accessing '%s': %w", indexesPath, err) + } + + spinnerCfg := yacspin.Config{ + Frequency: 100 * time.Millisecond, + CharSet: yacspin.CharSets[11], + StopCharacter: "✓", + StopColors: []string{"fgGreen"}, + StopFailCharacter: "✗", + StopFailColors: []string{"fgRed"}, + } + + spinner, err = yacspin.New(spinnerCfg) + err = spinner.Start() + if err != nil { + return fmt.Errorf("error creating spinner: %w", err) + } + + checkSpaces(filepath.Join(rootPath, "users")) + spinner.Suffix(" Personal spaces check ") + spinner.StopMessage("completed") + spinner.Stop() + + checkSpaces(filepath.Join(rootPath, "projects")) + spinner.Suffix(" Project spaces check ") + spinner.StopMessage("completed") + spinner.Stop() + + if restartRequired { + fmt.Println("\n\n ⚠️ Please restart your openCloud instance to apply changes.") + } + return nil +} + +func checkSpaces(basePath string) { + dirEntries, err := os.ReadDir(basePath) + if err != nil { + spinner.Message(fmt.Sprintf("Error reading spaces directory '%s'\n", basePath)) + spinner.StopFail() + return + } + + for _, entry := range dirEntries { + if entry.IsDir() { + fullPath := filepath.Join(basePath, entry.Name()) + checkSpace(fullPath) + } + } +} + +func checkSpace(spacePath string) { + spinner.Suffix(fmt.Sprintf(" Checking space '%s'", spacePath)) + + info, err := os.Stat(spacePath) + if err != nil { + logFailure("Error accessing path '%s': %v", spacePath, err) + return + } + if !info.IsDir() { + logFailure("Error: The provided path '%s' is not a directory\n", spacePath) + return + } + + spaceID, err := xattr.Get(spacePath, spaceIDAttrName) + if err != nil || len(spaceID) == 0 { + logFailure("Error: The directory '%s' does not seem to be a space root, it's missing the '%s' attribute\n", spacePath, spaceIDAttrName) + return + } + + checkSpaceID(spacePath) +} + +func checkSpaceID(spacePath string) { + spinner.Message("checking space ID uniqueness") + + entries, uniqueIDs, oldestEntry, err := gatherAttributes(spacePath) + if err != nil { + logFailure("Failed to gather attributes: %v", err) + return + } + + if len(entries) == 0 { + logSuccess("(empty space)") + return + } + + if len(uniqueIDs) > 1 { + spinner.Pause() + fmt.Println("\n ⚠ Multiple space IDs found:") + for id := range uniqueIDs { + fmt.Printf(" - %s\n", id) + } + + fmt.Printf("\n ⏳ Oldest entry is '%s' (modified on %s).\n", + filepath.Base(oldestEntry.Path), oldestEntry.ModTime.Format(time.RFC1123)) + + targetID := oldestEntry.ParentID + fmt.Printf(" ✅ Proposed target Parent ID: %s\n", targetID) + + fmt.Printf("\n Do you want to unify all parent IDs to '%s'? This will modify %d entries, the directory, and the user index. (y/N): ", targetID, len(entries)) + + reader := bufio.NewReader(os.Stdin) + input, _ := reader.ReadString('\n') + input = strings.TrimSpace(strings.ToLower(input)) + + if input != "y" { + spinner.Unpause() + logFailure("Operation cancelled by user.") + return + } + restartRequired = true + fixSpaceID(spacePath, targetID, entries) + spinner.Unpause() + } else { + logSuccess("") + } +} + +func fixSpaceID(spacePath, targetID string, entries []EntryInfo) { + // Set all parentid attributes to the proper space ID + err := setAllParentIDAttributes(entries, targetID) + if err != nil { + logFailure("an error occurred during file attribute update: %v", err) + return + } + + // Update space ID itself + fmt.Printf(" Updating directory '%s' with attribute '%s' -> %s\n", filepath.Base(spacePath), idAttrName, targetID) + err = xattr.Set(spacePath, idAttrName, []byte(targetID)) + if err != nil { + logFailure("Failed to set attribute on directory '%s': %v", spacePath, err) + return + } + err = xattr.Set(spacePath, spaceIDAttrName, []byte(targetID)) + if err != nil { + logFailure("Failed to set attribute on directory '%s': %v", spacePath, err) + return + } + + // update the index + err = updateOwnerIndexFile(spacePath, targetID) + if err != nil { + logFailure("Could not update the owner index file: %v", err) + } +} + +func gatherAttributes(path string) ([]EntryInfo, map[string]struct{}, EntryInfo, error) { + dirEntries, err := os.ReadDir(path) + if err != nil { + return nil, nil, EntryInfo{}, fmt.Errorf("failed to read directory: %w", err) + } + + var allEntries []EntryInfo + uniqueIDs := make(map[string]struct{}) + var oldestEntry EntryInfo + oldestTime := time.Now().Add(100 * 365 * 24 * time.Hour) // Set to a future date to find the oldest entry + + for _, entry := range dirEntries { + fullPath := filepath.Join(path, entry.Name()) + info, err := os.Stat(fullPath) + if err != nil { + fmt.Printf(" - Warning: could not stat %s: %v\n", entry.Name(), err) + continue + } + + parentID, err := xattr.Get(fullPath, parentIDAttrName) + if err != nil { + continue // Skip if attribute doesn't exist or can't be read + } + + entryInfo := EntryInfo{ + Path: fullPath, + ModTime: info.ModTime(), + ParentID: string(parentID), + } + + allEntries = append(allEntries, entryInfo) + uniqueIDs[string(parentID)] = struct{}{} + + if entryInfo.ModTime.Before(oldestTime) { + oldestTime = entryInfo.ModTime + oldestEntry = entryInfo + } + } + + return allEntries, uniqueIDs, oldestEntry, nil +} + +func setAllParentIDAttributes(entries []EntryInfo, targetID string) error { + fmt.Printf(" Setting all parent IDs to '%s':\n", targetID) + + for _, entry := range entries { + if entry.ParentID == targetID { + fmt.Printf(" - Skipping '%s' (already has target ID).\n", filepath.Base(entry.Path)) + continue + } + + fmt.Printf(" - Removing all attributes from '%s'. It will be re-assimilated\n", filepath.Base(entry.Path)) + filepath.WalkDir(entry.Path, func(path string, d os.DirEntry, err error) error { + if err != nil { + return fmt.Errorf("error walking path '%s': %w", path, err) + } + + // Remove all attributes from the file. + if err := removeAttributes(path); err != nil { + fmt.Printf("failed to remove attributes from '%s': %v", path, err) + } + return nil + }) + } + return nil +} + +// updateOwnerIndexFile handles the logic of reading, modifying, and writing the MessagePack index file. +func updateOwnerIndexFile(basePath string, targetID string) error { + fmt.Printf(" Rewriting index file '%s'\n", basePath) + + ownerID, err := xattr.Get(basePath, ownerIDAttrName) + if err != nil { + return fmt.Errorf("could not get owner ID from oldest entry '%s' to find index: %w", basePath, err) + } + + indexPath := filepath.Join(basePath, "../../indexes/by-user-id", string(ownerID)+".mpk") + indexPath = filepath.Clean(indexPath) + + // Read the MessagePack file + fileData, err := os.ReadFile(indexPath) + if err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("index file does not exist, skipping update") + } + return fmt.Errorf("could not read index file: %w", err) + } + var indexMap map[string]string + if err := msgpack.Unmarshal(fileData, &indexMap); err != nil { + return fmt.Errorf("failed to parse MessagePack index file (is it corrupt?): %w", err) + } + + // Remove obsolete IDs from the map + itemsRemoved := 0 + for id := range indexMap { + if id != targetID { + if _, ok := indexMap[id]; ok { + delete(indexMap, id) + itemsRemoved++ + fmt.Printf(" - Removing obsolete ID '%s' from index.\n", id) + } + } + } + + if itemsRemoved == 0 { + fmt.Printf(" No obsolete IDs found in the index file. Nothing to change.\n") + return nil + } + + // Write the data back to the file + updatedData, err := msgpack.Marshal(&indexMap) + if err != nil { + return fmt.Errorf("failed to marshal updated index map: %w", err) + } + if err := os.WriteFile(indexPath, updatedData, 0644); err != nil { + return fmt.Errorf("failed to write updated index file: %w", err) + } + + logSuccess("Successfully removed %d item(s) and saved index file.\n", itemsRemoved) + return nil +} + +func removeAttributes(path string) error { + attrNames, err := xattr.List(path) + if err != nil { + return fmt.Errorf("failed to list attributes for '%s': %w", path, err) + } + + for _, attrName := range attrNames { + if err := xattr.Remove(path, attrName); err != nil { + return fmt.Errorf("failed to remove attribute '%s' from '%s': %w", attrName, path, err) + } + } + return nil +} + +func logFailure(message string, args ...any) { + spinner.StopFailMessage(fmt.Sprintf(message, args...)) + spinner.StopFail() + spinner.Start() +} + +func logSuccess(message string, args ...any) { + spinner.StopMessage(fmt.Sprintf(message, args...)) + spinner.Stop() + spinner.Start() +} diff --git a/vendor/github.com/theckman/yacspin/.gitignore b/vendor/github.com/theckman/yacspin/.gitignore new file mode 100644 index 000000000..fa3fbdf51 --- /dev/null +++ b/vendor/github.com/theckman/yacspin/.gitignore @@ -0,0 +1,25 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool +*.out +coverage.txt + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# README GIF generation files +*.mov +*.mp4 +*.gif +*.prproj + +# OS files +.DS_Store diff --git a/vendor/github.com/theckman/yacspin/.golangci.yaml b/vendor/github.com/theckman/yacspin/.golangci.yaml new file mode 100644 index 000000000..31b112318 --- /dev/null +++ b/vendor/github.com/theckman/yacspin/.golangci.yaml @@ -0,0 +1,71 @@ +run: + tests: true + +# all available settings of specific linters +linters-settings: + govet: + # report about shadowed variables + check-shadowing: true + gofmt: + # simplify code: gofmt with `-s` option, true by default + simplify: true + dupl: + # tokens count to trigger issue, 150 by default + threshold: 100 + goconst: + # minimal length of string constant, 3 by default + min-len: 3 + # minimal occurrences count to trigger, 3 by default + min-occurrences: 3 + misspell: + # Correct spellings using locale preferences for US or UK. + # Default is to use a neutral variety of English. + # Setting locale to US will correct the British spelling of 'colour' to 'color'. + locale: US + staticcheck: + checks: [ "all" ] + revive: + confidence: 0.8 + ignore-generated-header: true + rules: + - name: context-keys-type + - name: time-naming + - name: var-declaration + - name: unexported-return + - name: errorf + - name: blank-imports + - name: context-as-argument + - name: dot-imports + - name: error-return + - name: error-strings + - name: error-naming + - name: exported + - name: increment-decrement + - name: var-naming + - name: package-comments + - name: range + - name: receiver-naming + - name: indent-error-flow + - name: superfluous-else + - name: struct-tag + - name: modifies-value-receiver + - name: range-val-in-closure + - name: range-val-address + - name: atomic + - name: empty-lines + - name: early-return + - name: useless-break + +linters: + enable: + - revive + - govet + - gosec + - staticcheck + - typecheck + fast: false + +issues: + exclude-use-default: false + exclude: + - G104 diff --git a/vendor/github.com/theckman/yacspin/LICENSE b/vendor/github.com/theckman/yacspin/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/vendor/github.com/theckman/yacspin/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/theckman/yacspin/README.md b/vendor/github.com/theckman/yacspin/README.md new file mode 100644 index 000000000..c7c21b1f0 --- /dev/null +++ b/vendor/github.com/theckman/yacspin/README.md @@ -0,0 +1,274 @@ +# Yet Another CLi Spinner (for Go) +[![License](https://img.shields.io/github/license/theckman/yacspin.svg)](https://github.com/theckman/yacspin/blob/master/LICENSE) +[![GoDoc](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/theckman/yacspin) +[![Latest Git Tag](https://img.shields.io/github/tag/theckman/yacspin.svg)](https://github.com/theckman/yacspin/releases) +[![GitHub Actions master Build Status](https://github.com/theckman/yacspin/actions/workflows/tests.yaml/badge.svg?branch=master)](https://github.com/theckman/yacspin/actions/workflows/tests.yaml) +[![Go Report Card](https://goreportcard.com/badge/github.com/theckman/yacspin)](https://goreportcard.com/report/github.com/theckman/yacspin) +[![Codecov](https://img.shields.io/codecov/c/github/theckman/yacspin)](https://codecov.io/gh/theckman/yacspin) + +Package `yacspin` provides yet another CLi spinner for Go, taking inspiration +(and some utility code) from the https://github.com/briandowns/spinner project. +Specifically `yacspin` borrows the default character sets, and color mappings to +github.com/fatih/color colors, from that project. + +## License +Because this package adopts the spinner character sets from https://github.com/briandowns/spinner, +this package is released under the Apache 2.0 License. + +## Yet Another CLi Spinner? +This project was created after it was realized that the most popular spinner +library for Go had some limitations, that couldn't be fixed without a massive +overhaul of the API. + +The other spinner ties the ability to show updated messages to the spinner's +animation, meaning you can't always show all the information you want to the end +user without changing the animation speed. This means you need to trade off +animation aesthetics to show "realtime" information. It was a goal to avoid this +problem. + +In addition, there were also some API design choices that have made it unsafe +for concurrent use, which presents challenges when trying to update the text in +the spinner while it's animating. This could result in undefined behavior due to +data races. + +There were also some variable-width spinners in that other project that did +not render correctly. Because the width of the spinner animation would change, +so would the position of the message on the screen. `yacspin` uses a dynamic +width when animating, so your message should appear static relative to the +animating spinner. + +Finally, there was an interest in the spinner being able to represent a task, and to +indicate whether it failed or was successful. This would have further compounded +the API changes needed above to support in an intuitive way. + +This project takes inspiration from that other project, and takes a new approach +to address the challenges above. + +## Features +#### Provided Spinners +There are over 90 spinners available in the `CharSets` package variable. They +were borrowed from [github.com/briandowns/spinner](https://github.com/briandowns/spinner). +There is a table with most of the spinners [at the bottom of this README](#Spinners). + +#### Dynamic Width of Animation +Because of how some spinners are animated, they may have different widths are +different times in the animation. `yacspin` calculates the maximum width, and +pads the animation to ensure the text's position on the screen doesn't change. +This results in a smoother looking animation. + +##### yacspin +![yacspin animation with dynamic width](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/features/width_good.gif) + +##### other spinners +![other spinners' animation with dynamic width](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/features/width_bad.gif) + +#### Success and Failure Results +The spinner has both a `Stop()` and `StopFail()` method, which allows the +spinner to result in a success message or a failure message. The messages, +colors, and even the character used to denote success or failure are +customizable in either the initial config or via the spinner's methods. + +By doing this you can use a single `yacspin` spinner to display the status of a +list of tasks being executed serially. + +##### Stop +![Animation with Success](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/features/stop.gif) + +##### StopFail +![Animation with Failure](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/features/stop_fail.gif) + +#### Animation At End of Line +The `SpinnerAtEnd` field of the `Config` struct allows you to specify whether +the spinner is rendered at the end of the line instead of the beginning. The +default value (`false`) results in the spinner being rendered at the beginning +of the line. + +#### Concurrency +The spinner is safe for concurrent use, so you can update any of its settings +via methods whether the spinner is stopped or is currently animating. + +#### Live Updates +Most spinners tie the ability to show new messages with the animation of the +spinner. So if the spinner animates every 200ms, you can only show updated +information every 200ms. If you wanted more frequent updates, you'd need to +tradeoff the asthetics of the animation to display more data. + +This spinner updates the printed information of the spinner immediately on +change, without the animation updating. This allows you to use an animation +speed that looks astheticaly pleasing, while also knowing the data presented to +the user will be updated live. + +You can see this in action in the following gif, where the filenames being +uploaded are rendered independent of the spinner being animated: + +![Animation with Success](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/features/stop.gif) + +#### Pausing for Updates +Sometimes you want to change a few settings, and don't want the spinner to +render your partially applied configuration. If your spinner is running, and you +want to change a few configuration items via method calls, you can `Pause()` the +spinner first. After making the changes you can call `Unpause()`, and it will +continue rendering like normal with the newly applied configuration. + +#### Supporting Non-Interactive (TTY) Output Targets +`yacspin` also has native support for non-interactive (TTY) output targets. By +default this is detected in the constructor, or can be overriden via the +`TerminalMode` `Config` struct field. When detecting the application is not +running withn a TTY session, the behavior of the spinner is different. + +Specifically, when this is automatically detected the spinner no longer uses +colors, disables the automatic spinner animation, and instead only animates the +spinner when updating the message. In addition, each animation is rendered on a +new line instead of overwriting the current line. + +This should result in human-readable output without any changes needed by +consumers, even when the system is writing to a non-TTY destination. + +#### Manually Stepping Animation +If you'd like to manually animate the spinner, you can do so by setting the +`TerminalMode` to `ForceNoTTYMode | ForceSmartTerminalMode`. In this mode the +spinner will still use colors and other text stylings, but the animation only +happens when data is updated and on individual lines. You can accomplish this by +calling the `Message()` method with the same used previously. + +## Usage +``` +go get github.com/theckman/yacspin +``` + +Within the `yacspin` package there are some default spinners stored in the +`yacspin.CharSets` variable, and you can also provide your own. There is also a +list of known colors in the `yacspin.ValidColors` variable. + +### Example + +There are runnable examples in the [examples/](https://github.com/theckman/yacspin/tree/master/examples) +directory, with one simple example and one more advanced one. Here is a quick +snippet showing usage from a very high level, with error handling omitted: + +```Go +cfg := yacspin.Config{ + Frequency: 100 * time.Millisecond, + CharSet: yacspin.CharSets[59], + Suffix: " backing up database to S3", + SuffixAutoColon: true, + Message: "exporting data", + StopCharacter: "✓", + StopColors: []string{"fgGreen"}, +} + +spinner, err := yacspin.New(cfg) +// handle the error + +err = spinner.Start() + +// doing some work +time.Sleep(2 * time.Second) + +spinner.Message("uploading data") + +// upload... +time.Sleep(2 * time.Second) + +err = spinner.Stop() +``` + +## Spinners + +The spinner animations below are recorded at a refresh frequency of 200ms. Some +animations may look better at a different speed, so play around with the +frequency until you find a value you find aesthetically pleasing. + +yacspin.CharSets index | sample gif (Frequency: 200ms) +-----------------------|------------------------------ +0 | ![0 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/0.gif) +1 | ![1 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/1.gif) +2 | ![2 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/2.gif) +3 | ![3 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/3.gif) +4 | ![4 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/4.gif) +5 | ![5 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/5.gif) +6 | ![6 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/6.gif) +7 | ![7 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/7.gif) +8 | ![8 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/8.gif) +9 | ![9 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/9.gif) +10 | ![10 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/10.gif) +11 | ![11 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/11.gif) +12 | ![12 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/12.gif) +13 | ![13 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/13.gif) +14 | ![14 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/14.gif) +15 | ![15 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/15.gif) +16 | ![16 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/16.gif) +17 | ![17 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/17.gif) +18 | ![18 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/18.gif) +19 | ![19 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/19.gif) +20 | ![20 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/20.gif) +21 | ![21 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/21.gif) +22 | ![22 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/22.gif) +23 | ![23 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/23.gif) +24 | ![24 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/24.gif) +25 | ![25 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/25.gif) +26 | ![26 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/26.gif) +27 | ![27 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/27.gif) +28 | ![28 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/28.gif) +29 | ![29 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/29.gif) +30 | ![30 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/30.gif) +31 | ![31 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/31.gif) +32 | ![32 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/32.gif) +33 | ![33 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/33.gif) +34 | ![34 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/34.gif) +35 | ![35 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/35.gif) +36 | ![36 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/36.gif) +37 | ![37 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/37.gif) +38 | ![38 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/38.gif) +39 | ![39 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/39.gif) +40 | ![40 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/40.gif) +41 | ![41 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/41.gif) +42 | ![42 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/42.gif) +43 | ![43 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/43.gif) +44 | ![44 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/44.gif) +45 | ![45 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/45.gif) +46 | ![46 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/46.gif) +47 | ![47 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/47.gif) +48 | ![48 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/48.gif) +49 | ![49 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/49.gif) +50 | ![50 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/50.gif) +51 | ![51 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/51.gif) +52 | ![52 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/52.gif) +53 | ![53 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/53.gif) +54 | ![54 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/54.gif) +55 | ![55 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/55.gif) +56 | ![56 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/56.gif) +57 | ![57 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/57.gif) +58 | ![58 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/58.gif) +59 | ![59 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/59.gif) +60 | ![60 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/60.gif) +61 | ![61 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/61.gif) +62 | ![62 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/62.gif) +63 | ![63 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/63.gif) +64 | ![64 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/64.gif) +65 | ![65 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/65.gif) +66 | ![66 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/66.gif) +67 | ![67 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/67.gif) +68 | ![68 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/68.gif) +69 | ![69 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/69.gif) +70 | ![70 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/70.gif) +71 | ![71 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/71.gif) +72 | ![72 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/72.gif) +73 | ![73 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/73.gif) +74 | ![74 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/74.gif) +75 | ![75 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/75.gif) +76 | ![76 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/76.gif) +77 | ![77 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/77.gif) +78 | ![78 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/78.gif) +79 | ![79 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/79.gif) +80 | ![80 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/80.gif) +81 | ![81 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/81.gif) +82 | ![82 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/82.gif) +83 | ![83 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/83.gif) +84 | ![84 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/84.gif) +85 | ![85 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/85.gif) +86 | ![86 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/86.gif) +87 | ![87 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/87.gif) +88 | ![88 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/88.gif) +89 | ![89 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/89.gif) +90 | ![90 gif](https://raw.githubusercontent.com/theckman/yacspin-gifs/11953a4f12560eaf4a27054d3adad471eb19193c/spinners/90.gif) diff --git a/vendor/github.com/theckman/yacspin/character_sets.go b/vendor/github.com/theckman/yacspin/character_sets.go new file mode 100644 index 000000000..7fb5990f7 --- /dev/null +++ b/vendor/github.com/theckman/yacspin/character_sets.go @@ -0,0 +1,121 @@ +// Copyright (c) 2021 Brian J. Downs +// Copyright (c) 2019-2021 Tim Heckman +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Please see the LICENSE file for the copy of the Apache 2.0 License. +// +// This file was copied from: https://github.com/briandowns/spinner +// +// Modifications: +// +// - removed runtime generation of CharSets 37 and 38; made them literals +// - fixed pipe spinner (32) animation, by adding missing frame + +package yacspin + +// CharSets contains the default character sets from +// https://github.com/briandowns/spinner. +var CharSets = map[int][]string{ + 0: {"←", "↖", "↑", "↗", "→", "↘", "↓", "↙"}, + 1: {"▁", "▃", "▄", "▅", "▆", "▇", "█", "▇", "▆", "▅", "▄", "▃", "▁"}, + 2: {"▖", "▘", "▝", "▗"}, + 3: {"┤", "┘", "┴", "└", "├", "┌", "┬", "┐"}, + 4: {"◢", "◣", "◤", "◥"}, + 5: {"◰", "◳", "◲", "◱"}, + 6: {"◴", "◷", "◶", "◵"}, + 7: {"◐", "◓", "◑", "◒"}, + 8: {".", "o", "O", "@", "*"}, + 9: {"|", "/", "-", "\\"}, + 10: {"◡◡", "⊙⊙", "◠◠"}, + 11: {"⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"}, + 12: {">))'>", " >))'>", " >))'>", " >))'>", " >))'>", " <'((<", " <'((<", " <'((<"}, + 13: {"⠁", "⠂", "⠄", "⡀", "⢀", "⠠", "⠐", "⠈"}, + 14: {"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}, + 15: {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"}, + 16: {"▉", "▊", "▋", "▌", "▍", "▎", "▏", "▎", "▍", "▌", "▋", "▊", "▉"}, + 17: {"■", "□", "▪", "▫"}, + 18: {"←", "↑", "→", "↓"}, + 19: {"╫", "╪"}, + 20: {"⇐", "⇖", "⇑", "⇗", "⇒", "⇘", "⇓", "⇙"}, + 21: {"⠁", "⠁", "⠉", "⠙", "⠚", "⠒", "⠂", "⠂", "⠒", "⠲", "⠴", "⠤", "⠄", "⠄", "⠤", "⠠", "⠠", "⠤", "⠦", "⠖", "⠒", "⠐", "⠐", "⠒", "⠓", "⠋", "⠉", "⠈", "⠈"}, + 22: {"⠈", "⠉", "⠋", "⠓", "⠒", "⠐", "⠐", "⠒", "⠖", "⠦", "⠤", "⠠", "⠠", "⠤", "⠦", "⠖", "⠒", "⠐", "⠐", "⠒", "⠓", "⠋", "⠉", "⠈"}, + 23: {"⠁", "⠉", "⠙", "⠚", "⠒", "⠂", "⠂", "⠒", "⠲", "⠴", "⠤", "⠄", "⠄", "⠤", "⠴", "⠲", "⠒", "⠂", "⠂", "⠒", "⠚", "⠙", "⠉", "⠁"}, + 24: {"⠋", "⠙", "⠚", "⠒", "⠂", "⠂", "⠒", "⠲", "⠴", "⠦", "⠖", "⠒", "⠐", "⠐", "⠒", "⠓", "⠋"}, + 25: {"ヲ", "ァ", "ィ", "ゥ", "ェ", "ォ", "ャ", "ュ", "ョ", "ッ", "ア", "イ", "ウ", "エ", "オ", "カ", "キ", "ク", "ケ", "コ", "サ", "シ", "ス", "セ", "ソ", "タ", "チ", "ツ", "テ", "ト", "ナ", "ニ", "ヌ", "ネ", "ノ", "ハ", "ヒ", "フ", "ヘ", "ホ", "マ", "ミ", "ム", "メ", "モ", "ヤ", "ユ", "ヨ", "ラ", "リ", "ル", "レ", "ロ", "ワ", "ン"}, + 26: {".", "..", "..."}, + 27: {"▁", "▂", "▃", "▄", "▅", "▆", "▇", "█", "▉", "▊", "▋", "▌", "▍", "▎", "▏", "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█", "▇", "▆", "▅", "▄", "▃", "▂", "▁"}, + 28: {".", "o", "O", "°", "O", "o", "."}, + 29: {"+", "x"}, + 30: {"v", "<", "^", ">"}, + 31: {">>--->", " >>--->", " >>--->", " >>--->", " >>--->", " <---<<", " <---<<", " <---<<", " <---<<", "<---<<"}, + 32: {"|", "||", "|||", "||||", "|||||", "||||||", "|||||||", "||||||||", "|||||||", "||||||", "|||||", "||||", "|||", "||", "|"}, + 33: {"[ ]", "[= ]", "[== ]", "[=== ]", "[==== ]", "[===== ]", "[====== ]", "[======= ]", "[======== ]", "[========= ]", "[==========]"}, + 34: {"(*---------)", "(-*--------)", "(--*-------)", "(---*------)", "(----*-----)", "(-----*----)", "(------*---)", "(-------*--)", "(--------*-)", "(---------*)"}, + 35: {"█▒▒▒▒▒▒▒▒▒", "███▒▒▒▒▒▒▒", "█████▒▒▒▒▒", "███████▒▒▒", "██████████"}, + 36: {"[ ]", "[=> ]", "[===> ]", "[=====> ]", "[======> ]", "[========> ]", "[==========> ]", "[============> ]", "[==============> ]", "[================> ]", "[==================> ]", "[===================>]"}, + 37: {"🕐", "🕑", "🕒", "🕓", "🕔", "🕕", "🕖", "🕗", "🕘", "🕙", "🕚", "🕛"}, // clock emoji: one per hour for hours 1~12 + 38: {"🕐", "🕜", "🕑", "🕝", "🕒", "🕞", "🕓", "🕟", "🕔", "🕠", "🕕", "🕡", "🕖", "🕢", "🕗", "🕣", "🕘", "🕤", "🕙", "🕥", "🕚", "🕦", "🕛", "🕧"}, // clock emoji: one per half hour for hours 1~12 + 39: {"🌍", "🌎", "🌏"}, + 40: {"◜", "◝", "◞", "◟"}, + 41: {"⬒", "⬔", "⬓", "⬕"}, + 42: {"⬖", "⬘", "⬗", "⬙"}, + 43: {"[>>> >]", "[]>>>> []", "[] >>>> []", "[] >>>> []", "[] >>>> []", "[] >>>>[]", "[>> >>]"}, + 44: {"♠", "♣", "♥", "♦"}, + 45: {"➞", "➟", "➠", "➡", "➠", "➟"}, + 46: {" | ", ` \ `, "_ ", ` \ `, " | ", " / ", " _", " / "}, + 47: {" . . . .", ". . . .", ". . . .", ". . . .", ". . . . ", ". . . . ."}, + 48: {" | ", " / ", " _ ", ` \ `, " | ", ` \ `, " _ ", " / "}, + 49: {"⎺", "⎻", "⎼", "⎽", "⎼", "⎻"}, + 50: {"▹▹▹▹▹", "▸▹▹▹▹", "▹▸▹▹▹", "▹▹▸▹▹", "▹▹▹▸▹", "▹▹▹▹▸"}, + 51: {"[ ]", "[ =]", "[ ==]", "[ ===]", "[====]", "[=== ]", "[== ]", "[= ]"}, + 52: {"( ● )", "( ● )", "( ● )", "( ● )", "( ●)", "( ● )", "( ● )", "( ● )", "( ● )"}, + 53: {"✶", "✸", "✹", "✺", "✹", "✷"}, + 54: {"▐|\\____________▌", "▐_|\\___________▌", "▐__|\\__________▌", "▐___|\\_________▌", "▐____|\\________▌", "▐_____|\\_______▌", "▐______|\\______▌", "▐_______|\\_____▌", "▐________|\\____▌", "▐_________|\\___▌", "▐__________|\\__▌", "▐___________|\\_▌", "▐____________|\\▌", "▐____________/|▌", "▐___________/|_▌", "▐__________/|__▌", "▐_________/|___▌", "▐________/|____▌", "▐_______/|_____▌", "▐______/|______▌", "▐_____/|_______▌", "▐____/|________▌", "▐___/|_________▌", "▐__/|__________▌", "▐_/|___________▌", "▐/|____________▌"}, + 55: {"▐⠂ ▌", "▐⠈ ▌", "▐ ⠂ ▌", "▐ ⠠ ▌", "▐ ⡀ ▌", "▐ ⠠ ▌", "▐ ⠂ ▌", "▐ ⠈ ▌", "▐ ⠂ ▌", "▐ ⠠ ▌", "▐ ⡀ ▌", "▐ ⠠ ▌", "▐ ⠂ ▌", "▐ ⠈ ▌", "▐ ⠂▌", "▐ ⠠▌", "▐ ⡀▌", "▐ ⠠ ▌", "▐ ⠂ ▌", "▐ ⠈ ▌", "▐ ⠂ ▌", "▐ ⠠ ▌", "▐ ⡀ ▌", "▐ ⠠ ▌", "▐ ⠂ ▌", "▐ ⠈ ▌", "▐ ⠂ ▌", "▐ ⠠ ▌", "▐ ⡀ ▌", "▐⠠ ▌"}, + 56: {"¿", "?"}, + 57: {"⢹", "⢺", "⢼", "⣸", "⣇", "⡧", "⡗", "⡏"}, + 58: {"⢄", "⢂", "⢁", "⡁", "⡈", "⡐", "⡠"}, + 59: {". ", ".. ", "...", " ..", " .", " "}, + 60: {".", "o", "O", "°", "O", "o", "."}, + 61: {"▓", "▒", "░"}, + 62: {"▌", "▀", "▐", "▄"}, + 63: {"⊶", "⊷"}, + 64: {"▪", "▫"}, + 65: {"□", "■"}, + 66: {"▮", "▯"}, + 67: {"-", "=", "≡"}, + 68: {"d", "q", "p", "b"}, + 69: {"∙∙∙", "●∙∙", "∙●∙", "∙∙●", "∙∙∙"}, + 70: {"🌑 ", "🌒 ", "🌓 ", "🌔 ", "🌕 ", "🌖 ", "🌗 ", "🌘 "}, + 71: {"☗", "☖"}, + 72: {"⧇", "⧆"}, + 73: {"◉", "◎"}, + 74: {"㊂", "㊀", "㊁"}, + 75: {"⦾", "⦿"}, + 76: {"ဝ", "၀"}, + 77: {"▌", "▀", "▐▄"}, + 78: {"⠈⠁", "⠈⠑", "⠈⠱", "⠈⡱", "⢀⡱", "⢄⡱", "⢄⡱", "⢆⡱", "⢎⡱", "⢎⡰", "⢎⡠", "⢎⡀", "⢎⠁", "⠎⠁", "⠊⠁"}, + 79: {"________", "-_______", "_-______", "__-_____", "___-____", "____-___", "_____-__", "______-_", "_______-", "________", "_______-", "______-_", "_____-__", "____-___", "___-____", "__-_____", "_-______", "-_______", "________"}, + 80: {"|_______", "_/______", "__-_____", "___\\____", "____|___", "_____/__", "______-_", "_______\\", "_______|", "______\\_", "_____-__", "____/___", "___|____", "__\\_____", "_-______"}, + 81: {"□", "◱", "◧", "▣", "■"}, + 82: {"□", "◱", "▨", "▩", "■"}, + 83: {"░", "▒", "▓", "█"}, + 84: {"░", "█"}, + 85: {"⚪", "⚫"}, + 86: {"◯", "⬤"}, + 87: {"▱", "▰"}, + 88: {"➊", "➋", "➌", "➍", "➎", "➏", "➐", "➑", "➒", "➓"}, + 89: {"½", "⅓", "⅔", "¼", "¾", "⅛", "⅜", "⅝", "⅞"}, + 90: {"↞", "↟", "↠", "↡"}, +} diff --git a/vendor/github.com/theckman/yacspin/colors.go b/vendor/github.com/theckman/yacspin/colors.go new file mode 100644 index 000000000..1a0bc8a62 --- /dev/null +++ b/vendor/github.com/theckman/yacspin/colors.go @@ -0,0 +1,174 @@ +// This file is available under the Apache 2.0 License +// This file was copied from: https://github.com/briandowns/spinner +// +// Please see the LICENSE file for the copy of the Apache 2.0 License. +// +// Modifications: +// +// - made validColors set map more idiomatic with an empty struct value +// - added a function for creating color functions from color list + +package yacspin + +import ( + "fmt" + + "github.com/fatih/color" +) + +// ValidColors holds the list of the strings that are mapped to +// github.com/fatih/color color attributes. Any of these colors / attributes can +// be used with the *Spinner type, and it should be reflected in the output. +var ValidColors = map[string]struct{}{ + // default colors for backwards compatibility + "black": {}, + "red": {}, + "green": {}, + "yellow": {}, + "blue": {}, + "magenta": {}, + "cyan": {}, + "white": {}, + + // attributes + "reset": {}, + "bold": {}, + "faint": {}, + "italic": {}, + "underline": {}, + "blinkslow": {}, + "blinkrapid": {}, + "reversevideo": {}, + "concealed": {}, + "crossedout": {}, + + // foreground text + "fgBlack": {}, + "fgRed": {}, + "fgGreen": {}, + "fgYellow": {}, + "fgBlue": {}, + "fgMagenta": {}, + "fgCyan": {}, + "fgWhite": {}, + + // foreground Hi-Intensity text + "fgHiBlack": {}, + "fgHiRed": {}, + "fgHiGreen": {}, + "fgHiYellow": {}, + "fgHiBlue": {}, + "fgHiMagenta": {}, + "fgHiCyan": {}, + "fgHiWhite": {}, + + // background text + "bgBlack": {}, + "bgRed": {}, + "bgGreen": {}, + "bgYellow": {}, + "bgBlue": {}, + "bgMagenta": {}, + "bgCyan": {}, + "bgWhite": {}, + + // background Hi-Intensity text + "bgHiBlack": {}, + "bgHiRed": {}, + "bgHiGreen": {}, + "bgHiYellow": {}, + "bgHiBlue": {}, + "bgHiMagenta": {}, + "bgHiCyan": {}, + "bgHiWhite": {}, +} + +// returns a valid color's foreground text color attribute +var colorAttributeMap = map[string]color.Attribute{ + // default colors for backwards compatibility + "black": color.FgBlack, + "red": color.FgRed, + "green": color.FgGreen, + "yellow": color.FgYellow, + "blue": color.FgBlue, + "magenta": color.FgMagenta, + "cyan": color.FgCyan, + "white": color.FgWhite, + + // attributes + "reset": color.Reset, + "bold": color.Bold, + "faint": color.Faint, + "italic": color.Italic, + "underline": color.Underline, + "blinkslow": color.BlinkSlow, + "blinkrapid": color.BlinkRapid, + "reversevideo": color.ReverseVideo, + "concealed": color.Concealed, + "crossedout": color.CrossedOut, + + // foreground text colors + "fgBlack": color.FgBlack, + "fgRed": color.FgRed, + "fgGreen": color.FgGreen, + "fgYellow": color.FgYellow, + "fgBlue": color.FgBlue, + "fgMagenta": color.FgMagenta, + "fgCyan": color.FgCyan, + "fgWhite": color.FgWhite, + + // foreground Hi-Intensity text colors + "fgHiBlack": color.FgHiBlack, + "fgHiRed": color.FgHiRed, + "fgHiGreen": color.FgHiGreen, + "fgHiYellow": color.FgHiYellow, + "fgHiBlue": color.FgHiBlue, + "fgHiMagenta": color.FgHiMagenta, + "fgHiCyan": color.FgHiCyan, + "fgHiWhite": color.FgHiWhite, + + // background text colors + "bgBlack": color.BgBlack, + "bgRed": color.BgRed, + "bgGreen": color.BgGreen, + "bgYellow": color.BgYellow, + "bgBlue": color.BgBlue, + "bgMagenta": color.BgMagenta, + "bgCyan": color.BgCyan, + "bgWhite": color.BgWhite, + + // background Hi-Intensity text colors + "bgHiBlack": color.BgHiBlack, + "bgHiRed": color.BgHiRed, + "bgHiGreen": color.BgHiGreen, + "bgHiYellow": color.BgHiYellow, + "bgHiBlue": color.BgHiBlue, + "bgHiMagenta": color.BgHiMagenta, + "bgHiCyan": color.BgHiCyan, + "bgHiWhite": color.BgHiWhite, +} + +// validColor will make sure the given color is actually allowed +func validColor(c string) bool { + _, ok := ValidColors[c] + + return ok +} + +func colorFunc(colors ...string) (func(format string, a ...interface{}) string, error) { + if len(colors) == 0 { + return fmt.Sprintf, nil + } + + attrib := make([]color.Attribute, len(colors)) + + for i, color := range colors { + if !validColor(color) { + return nil, fmt.Errorf("%s is not a valid color", color) + } + + attrib[i] = colorAttributeMap[color] + } + + return color.New(attrib...).SprintfFunc(), nil +} diff --git a/vendor/github.com/theckman/yacspin/spinner.go b/vendor/github.com/theckman/yacspin/spinner.go new file mode 100644 index 000000000..dd96e5f3f --- /dev/null +++ b/vendor/github.com/theckman/yacspin/spinner.go @@ -0,0 +1,1191 @@ +// Package yacspin provides Yet Another CLi Spinner for Go, taking inspiration +// (and some utility code) from the https://github.com/briandowns/spinner +// project. Specifically this project borrows the default character sets, and +// color mappings to github.com/fatih/color colors, from that project. +// +// This spinner should support all major operating systems, and is tested +// against Linux, MacOS, and Windows. +// +// This spinner also supports an alternate mode of operation when the TERM +// environment variable is set to "dumb". This is discovered automatically when +// constructing the spinner. +// +// Within the yacspin package there are some default spinners stored in the +// yacspin.CharSets variable, and you can also provide your own. There is also a +// list of known colors in the yacspin.ValidColors variable, if you'd like to +// see what's supported. If you've used github.com/fatih/color before, they +// should look familiar. +// +// cfg := yacspin.Config{ +// Frequency: 100 * time.Millisecond, +// CharSet: yacspin.CharSets[59], +// Suffix: " backing up database to S3", +// Message: "exporting data", +// StopCharacter: "✓", +// StopColors: []string{"fgGreen"}, +// } +// +// spinner, err := yacspin.New(cfg) +// // handle the error +// +// spinner.Start() +// +// // doing some work +// time.Sleep(2 * time.Second) +// +// spinner.Message("uploading data") +// +// // upload... +// time.Sleep(2 * time.Second) +// +// spinner.Stop() +// +// Check out the Config struct to see all of the possible configuration options +// supported by the Spinner. +package yacspin + +import ( + "bytes" + "errors" + "fmt" + "io" + "math" + "os" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/mattn/go-colorable" + "github.com/mattn/go-isatty" + "github.com/mattn/go-runewidth" +) + +type character struct { + Value string + Size int +} + +func setToCharSlice(ss []string) ([]character, int) { + if len(ss) == 0 { + return nil, 0 + } + + var maxWidth int + c := make([]character, len(ss)) + + for i, s := range ss { + n := runewidth.StringWidth(s) + if n > maxWidth { + maxWidth = n + } + + c[i] = character{ + Value: s, + Size: n, + } + } + + return c, maxWidth +} + +// TerminalMode is a type to represent the bit flag controlling the terminal +// mode of the spinner, accepted as a field on the Config struct. See the +// comments on the exported constants for more info. +type TerminalMode uint32 + +const ( + // AutomaticMode configures the constructor function to try and determine if + // the application using yacspin is being executed within a interactive + // (teletype [TTY]) session. + AutomaticMode TerminalMode = 1 << iota + + // ForceTTYMode configures the spinner to operate as if it's running within + // a TTY session. + ForceTTYMode + + // ForceNoTTYMode configures the spinner to operate as if it's not running + // within a TTY session. This mode causes the spinner to only animate when + // data is being updated. Each animation is rendered on a new line. You can + // trigger an animation by calling the Message() method, including with the + // last value it was called with. + ForceNoTTYMode + + // ForceDumbTerminalMode configures the spinner to operate as if it's + // running within a dumb terminal. This means the spinner will not use ANSI + // escape sequences to print colors or to erase each line. Line erasure to + // animate the spinner is accomplished by overwriting the line with space + // characters. + ForceDumbTerminalMode + + // ForceSmartTerminalMode configures the spinner to operate as if it's + // running within a terminal that supports ANSI escape sequences (VT100). + // This includes printing of stylized text, and more better line erasure to + // animate the spinner. + ForceSmartTerminalMode +) + +func termModeAuto(t TerminalMode) bool { return t&AutomaticMode > 0 } +func termModeForceTTY(t TerminalMode) bool { return t&ForceTTYMode > 0 } +func termModeForceNoTTY(t TerminalMode) bool { return t&ForceNoTTYMode > 0 } +func termModeForceDumb(t TerminalMode) bool { return t&ForceDumbTerminalMode > 0 } +func termModeForceSmart(t TerminalMode) bool { return t&ForceSmartTerminalMode > 0 } + +// Config is the configuration structure for the Spinner type, which you provide +// to the New() function. Some of the fields can be updated after the *Spinner +// is constructed, others can only be set when calling the constructor. Please +// read the comments for those details. +type Config struct { + // Frequency specifies how often to animate the spinner. Optimal value + // depends on the character set you use. + Frequency time.Duration + + // Writer is the place where we are outputting the spinner, and can't be + // changed after the *Spinner has been constructed. If omitted (nil), this + // defaults to os.Stdout. + Writer io.Writer + + // ShowCursor specifies that the cursor should be shown by the spinner while + // animating. If it is not shown, the cursor will be restored when the + // spinner stops. This can't be changed after the *Spinner has been + // constructed. + // + // Please note, if you do not set this to true and the program crashes or is + // killed, you may need to reset your terminal for the cursor to appear + // again. + ShowCursor bool + + // HideCursor describes whether the cursor should be hidden by the spinner + // while animating. If it is hidden, it will be restored when the spinner + // stops. This can't be changed after the *Spinner has been constructed. + // + // Please note, if the program crashes or is killed you may need to reset + // your terminal for the cursor to appear again. + // + // Deprecated: use ShowCursor instead. + HideCursor bool + + // SpinnerAtEnd configures the spinner to render the animation at the end of + // the line instead of the beginning. The default behavior is to render the + // animated spinner at the beginning of the line. + SpinnerAtEnd bool + + // ColorAll describes whether to color everything (all) or just the spinner + // character(s). This cannot be changed after the *Spinner has been + // constructed. + ColorAll bool + + // Colors are the colors used for the different printed messages. This + // respects the ColorAll field. + Colors []string + + // CharSet is the list of characters to iterate through to draw the spinner. + CharSet []string + + // Prefix is the string printed immediately before the spinner. + // + // If SpinnerAtEnd is set to true, it's recommended that this string start + // with a space character (` `). + Prefix string + + // Suffix is the string printed immediately after the spinner and before the + // message. + // + // If SpinnerAtEnd is set to false, it's recommended that this string starts + // with an space character (` `). + Suffix string + + // SuffixAutoColon configures whether the spinner adds a colon after the + // suffix automatically. If there is a message, a colon followed by a space + // is added to the suffix. Otherwise, if there is no message, or the suffix + // is only space characters, the colon is omitted. + // + // If SpinnerAtEnd is set to true, this option is ignored. + SuffixAutoColon bool + + // Message is the message string printed by the spinner. If SpinnerAtEnd is + // set to false and SuffixAutoColon is set to true, the printed line will + // look like: + // + // : + // + // If SpinnerAtEnd is set to true, the printed line will instead look like + // this: + // + // + // + // In this case, it may be preferred to set the Prefix to empty space (` `). + Message string + + // StopMessage is the message used when Stop() is called. + StopMessage string + + // StopCharacter is spinner character used when Stop() is called. + // Recommended character is ✓, and can be more than just one character. + StopCharacter string + + // StopColors are the colors used for the Stop() printed line. This respects + // the ColorAll field. + StopColors []string + + // StopFailMessage is the message used when StopFail() is called. + StopFailMessage string + + // StopFailCharacter is the spinner character used when StopFail() is + // called. Recommended character is ✗, and can be more than just one + // character. + StopFailCharacter string + + // StopFailColors are the colors used for the StopFail() printed line. This + // respects the ColorAll field. + StopFailColors []string + + // TerminalMode is a bitflag field to control how the internal TTY / "dumb + // terminal" detection works, to allow consumers to override the internal + // behaviors. To set this value, it's recommended to use the TerminalMode + // constants exported by this package. + // + // If not set, the New() function implicitly sets it to AutomaticMode. The + // New() function also returns an error if you have conflicting flags, such + // as setting ForceTTYMode and ForceNoTTYMode, or if you set AutomaticMode + // and any other flags set. + // + // When in AutomaticMode, the New() function attempts to determine if the + // current application is running within an interactive (teletype [TTY]) + // session. If it does not appear to be within a TTY, it sets this field + // value to ForceNoTTYMode | ForceDumbTerminalMode. + // + // If this does appear to be a TTY, the ForceTTYMode bitflag will bet set. + // Similarly, if it's a TTY and the TERM environment variable isn't set to + // "dumb" the ForceSmartTerminalMode bitflag will also be set. + // + // If the deprecated NoTTY Config struct field is set to true, and this + // field is AutomaticMode, the New() function sets field to the value of + // ForceNoTTYMode | ForceDumbTerminalMode. + TerminalMode TerminalMode + + // NotTTY tells the spinner that the Writer should not be treated as a TTY. + // This results in the animation being disabled, with the animation only + // happening whenever the data is updated. This mode also renders each + // update on new line, versus reusing the current line. + // + // Deprecated: use TerminalMode field instead by setting it to: + // ForceNoTTYMode | ForceDumbTerminalMode. This will be removed in a future + // release. + NotTTY bool +} + +// Spinner is a type representing an animated CLi terminal spinner. The Spinner +// is constructed by the New() function of this package, which accepts a Config +// struct as the only argument. Some of the configuration values cannot be +// changed after the spinner is constructed, so be sure to read the comments +// within the Config type. +// +// Please note, by default the spinner will hide the terminal cursor when +// animating the spinner. If you do not set Config.ShowCursor to true, you need +// to make sure to call the Stop() or StopFail() method to reset the cursor in +// the terminal. Otherwise, after the program exits the cursor will be hidden +// and the user will need to `reset` their terminal. +type Spinner struct { + writer io.Writer + buffer *bytes.Buffer + colorAll bool + cursorHidden bool + suffixAutoColon bool + termMode TerminalMode + spinnerAtEnd bool + + status *uint32 + lastPrintLen int + cancelCh chan struct{} // send: Stop(), close: StopFail(); both stop painter + doneCh chan struct{} + pauseCh chan struct{} + unpauseCh chan struct{} + unpausedCh chan struct{} + + // mutex hat and the fields wearing it + mu *sync.Mutex + frequency time.Duration + chars []character + maxWidth int + index int + prefix string + suffix string + message string + colorFn func(format string, a ...interface{}) string + stopMsg string + stopChar character + stopColorFn func(format string, a ...interface{}) string + stopFailMsg string + stopFailChar character + stopFailColorFn func(format string, a ...interface{}) string + frequencyUpdateCh chan time.Duration + dataUpdateCh chan struct{} +} + +const ( + statusStopped uint32 = iota + statusStarting + statusRunning + statusStopping + statusPausing + statusPaused + statusUnpausing +) + +// New creates a new unstarted spinner. If stdout does not appear to be a TTY, +// this constructor implicitly sets cfg.NotTTY to true. +func New(cfg Config) (*Spinner, error) { + if cfg.ShowCursor && cfg.HideCursor { + return nil, errors.New("cfg.ShowCursor and cfg.HideCursor cannot be true") + } + + if cfg.TerminalMode == 0 { + cfg.TerminalMode = AutomaticMode + } + + // AutomaticMode flag has been set, but so have others + if termModeAuto(cfg.TerminalMode) && cfg.TerminalMode != AutomaticMode { + return nil, errors.New("cfg.TerminalMode cannot have AutomaticMode flag set if others are set") + } + + if termModeForceTTY(cfg.TerminalMode) && termModeForceNoTTY(cfg.TerminalMode) { + return nil, errors.New("cfg.TerminalMode cannot have both ForceTTYMode and ForceNoTTYMode flags set") + } + + if termModeForceDumb(cfg.TerminalMode) && termModeForceSmart(cfg.TerminalMode) { + return nil, errors.New("cfg.TerminalMode cannot have both ForceDumbTerminalMode and ForceSmartTerminalMode flags set") + } + + if cfg.HideCursor { + cfg.ShowCursor = false + } + + // cfg.NotTTY compatibility + if cfg.TerminalMode == AutomaticMode && cfg.NotTTY { + cfg.TerminalMode = ForceNoTTYMode | ForceDumbTerminalMode + } + + // is this a dumb terminal / not a TTY? + if cfg.TerminalMode == AutomaticMode && !isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd()) { + cfg.TerminalMode = ForceNoTTYMode | ForceDumbTerminalMode + } + + // if cfg.TerminalMode is still equal to AutomaticMode, this is a TTY + if cfg.TerminalMode == AutomaticMode { + cfg.TerminalMode = ForceTTYMode + + if os.Getenv("TERM") == "dumb" { + cfg.TerminalMode |= ForceDumbTerminalMode + } else { + cfg.TerminalMode |= ForceSmartTerminalMode + } + } + + buf := bytes.NewBuffer(make([]byte, 2048)) + buf.Reset() + + s := &Spinner{ + buffer: buf, + mu: &sync.Mutex{}, + frequency: cfg.Frequency, + status: uint32Ptr(0), + frequencyUpdateCh: make(chan time.Duration), // use unbuffered for now to avoid .Frequency() panic + dataUpdateCh: make(chan struct{}), + + colorAll: cfg.ColorAll, + cursorHidden: !cfg.ShowCursor, + spinnerAtEnd: cfg.SpinnerAtEnd, + suffixAutoColon: cfg.SuffixAutoColon, + termMode: cfg.TerminalMode, + colorFn: fmt.Sprintf, + stopColorFn: fmt.Sprintf, + stopFailColorFn: fmt.Sprintf, + } + + if err := s.Colors(cfg.Colors...); err != nil { + return nil, err + } + + if err := s.StopColors(cfg.StopColors...); err != nil { + return nil, err + } + + if err := s.StopFailColors(cfg.StopFailColors...); err != nil { + return nil, err + } + + if len(cfg.CharSet) == 0 { + cfg.CharSet = CharSets[9] + } + + // can only error if the charset is empty, and we prevent that above + _ = s.CharSet(cfg.CharSet) + + if termModeForceNoTTY(s.termMode) { + // hack to prevent the animation from running if not a TTY + s.frequency = time.Duration(math.MaxInt64) + } + + if cfg.Writer == nil { + cfg.Writer = colorable.NewColorableStdout() + } + + s.writer = cfg.Writer + + if len(cfg.Prefix) > 0 { + s.Prefix(cfg.Prefix) + } + + if len(cfg.Suffix) > 0 { + s.Suffix(cfg.Suffix) + } + + if len(cfg.Message) > 0 { + s.Message(cfg.Message) + } + + if len(cfg.StopMessage) > 0 { + s.StopMessage(cfg.StopMessage) + } + + if len(cfg.StopCharacter) > 0 { + s.StopCharacter(cfg.StopCharacter) + } + + if len(cfg.StopFailMessage) > 0 { + s.StopFailMessage(cfg.StopFailMessage) + } + + if len(cfg.StopFailCharacter) > 0 { + s.StopFailCharacter(cfg.StopFailCharacter) + } + + return s, nil +} + +func (s *Spinner) notifyDataChange() { + // non-blocking notification + select { + case s.dataUpdateCh <- struct{}{}: + default: + } +} + +// SpinnerStatus describes the status of the spinner. See the package constants +// for the list of all possible statuses +type SpinnerStatus uint32 + +const ( + // SpinnerStopped is a stopped spinner + SpinnerStopped SpinnerStatus = iota + + // SpinnerStarting is a starting spinner + SpinnerStarting + + // SpinnerRunning is a running spinner + SpinnerRunning + + // SpinnerStopping is a stopping spinner + SpinnerStopping + + // SpinnerPausing is a pausing spinner + SpinnerPausing + + // SpinnerPaused is a paused spinner + SpinnerPaused + + // SpinnerUnpausing is an unpausing spinner + SpinnerUnpausing +) + +func (s SpinnerStatus) String() string { + switch s { + case SpinnerStopped: + return "stopped" + case SpinnerStarting: + return "starting" + case SpinnerRunning: + return "running" + case SpinnerStopping: + return "stopping" + case SpinnerPausing: + return "pausing" + case SpinnerPaused: + return "paused" + case SpinnerUnpausing: + return "unpausing" + default: + return fmt.Sprintf("unknown (%d)", s) + } +} + +// Status returns the current status of the spinner. The returned value is of +// type SpinnerStatus, which can be compared against the exported Spinner* +// package-level constants (e.g., SpinnerRunning). +func (s *Spinner) Status() SpinnerStatus { + return SpinnerStatus(atomic.LoadUint32(s.status)) +} + +// Start begins the spinner on the Writer in the Config provided to New(). Only +// possible error is if the spinner is already runninng. +func (s *Spinner) Start() error { + // move us to the starting state + if !atomic.CompareAndSwapUint32(s.status, statusStopped, statusStarting) { + return errors.New("spinner already running or shutting down") + } + + // we now have atomic guarantees of no other goroutines starting or running + + s.mu.Lock() + + if s.frequency < 1 && termModeForceTTY(s.termMode) { + return errors.New("spinner Frequency duration must be greater than 0 when used within a TTY") + } + + if len(s.chars) == 0 { + s.mu.Unlock() + + // move us to the stopped state + if !atomic.CompareAndSwapUint32(s.status, statusStarting, statusStopped) { + panic("atomic invariant encountered") + } + + return errors.New("before starting the spinner a CharSet must be set") + } + + s.frequencyUpdateCh = make(chan time.Duration, 4) + s.dataUpdateCh, s.cancelCh = make(chan struct{}, 1), make(chan struct{}, 1) + + s.mu.Unlock() + + // because of the atomic swap above, we know it's safe to mutate these + // values outside of mutex + s.doneCh = make(chan struct{}) + s.pauseCh = make(chan struct{}) // unbuffered since we want this to be synchronous + + go s.painter(s.cancelCh, s.dataUpdateCh, s.pauseCh, s.doneCh, s.frequencyUpdateCh) + + // move us to the running state + if !atomic.CompareAndSwapUint32(s.status, statusStarting, statusRunning) { + panic("atomic invariant encountered") + } + + return nil +} + +// Pause puts the spinner in a state where it no longer animates or renders +// updates to data. This function blocks until the spinner's internal painting +// goroutine enters a paused state. +// +// If you want to make a few configuration changes and have them to appear at +// the same time, like changing the suffix, message, and color, you can Pause() +// the spinner first and then Unpause() after making the changes. +// +// If the spinner is not running (stopped, paused, or in transition to another +// state) this returns an error. +func (s *Spinner) Pause() error { + if !atomic.CompareAndSwapUint32(s.status, statusRunning, statusPausing) { + return errors.New("spinner not running") + } + + // set up the channels the painter will use + s.unpauseCh, s.unpausedCh = make(chan struct{}), make(chan struct{}) + + // inform the painter to pause as a blocking send + s.pauseCh <- struct{}{} + + if !atomic.CompareAndSwapUint32(s.status, statusPausing, statusPaused) { + panic("atomic invariant encountered") + } + + return nil +} + +// Unpause returns the spinner back to a running state after pausing. See +// Pause() documentation for more detail. This function blocks until the +// spinner's internal painting goroutine acknowledges the request to unpause. +// +// If the spinner is not paused this returns an error. +func (s *Spinner) Unpause() error { + if !atomic.CompareAndSwapUint32(s.status, statusPaused, statusUnpausing) { + return errors.New("spinner not paused") + } + + s.unpause() + + if !atomic.CompareAndSwapUint32(s.status, statusUnpausing, statusRunning) { + panic("atomic invariant encountered") + } + + return nil +} + +func (s *Spinner) unpause() { + // tell the painter to unpause + close(s.unpauseCh) + + // wait for the painter to signal it will continue + <-s.unpausedCh + + // clear the no longer needed channels + s.unpauseCh = nil + s.unpausedCh = nil +} + +// Stop disables the spinner, and prints the StopCharacter with the StopMessage +// using the StopColors. This blocks until the stopped message is printed. Only +// possible error is if the spinner is not running. +func (s *Spinner) Stop() error { + return s.stop(false) +} + +// StopFail disables the spinner, and prints the StopFailCharacter with the +// StopFailMessage using the StopFailColors. This blocks until the stopped +// message is printed. Only possible error is if the spinner is not running. +func (s *Spinner) StopFail() error { + return s.stop(true) +} + +func (s *Spinner) stop(fail bool) error { + // move us to a stopping state to protect against concurrent Stop() calls + wasRunning := atomic.CompareAndSwapUint32(s.status, statusRunning, statusStopping) + wasPaused := atomic.CompareAndSwapUint32(s.status, statusPaused, statusStopping) + + if !wasRunning && !wasPaused { + return errors.New("spinner not running or paused") + } + + // we now have an atomic guarantees of no other threads invoking state changes + + if !fail { + // this tells the painter to print the StopMessage and not the + // StopFailMessage + s.cancelCh <- struct{}{} + } + + close(s.cancelCh) + + if wasPaused { + s.unpause() + } + + // wait for the painter to stop + <-s.doneCh + + s.mu.Lock() + + s.dataUpdateCh = make(chan struct{}) // prevent panic() in various setter methods + s.frequencyUpdateCh = make(chan time.Duration) // prevent panic() in .Frequency() + + s.mu.Unlock() + + // because of atomic swaps and channel receive above we know it's + // safe to mutate these fields outside of the mutex + s.index = 0 + s.cancelCh = nil + s.doneCh = nil + s.pauseCh = nil + + // move us to the stopped state + if !atomic.CompareAndSwapUint32(s.status, statusStopping, statusStopped) { + panic("atomic invariant encountered") + } + + return nil +} + +// handleFrequencyUpdate is for when the frequency was changed. This tries to +// see if we should fire the timer now, or change its current duration to match +// the new duration. +func handleFrequencyUpdate(newFrequency time.Duration, timer *time.Timer, lastTick time.Time) { + // if timer fired, drain the channel + if !timer.Stop() { + timerLoop: + for { + select { + case <-timer.C: + default: + break timerLoop + } + } + } + + timeSince := time.Since(lastTick) + + // if we've exceeded the new delay trigger timer immediately + if timeSince >= newFrequency { + timer.Reset(0) + return + } + + timer.Reset(newFrequency - timeSince) +} + +func (s *Spinner) painter(cancel, dataUpdate, pause <-chan struct{}, done chan<- struct{}, frequencyUpdate <-chan time.Duration) { + timer := time.NewTimer(0) + var lastTick time.Time + + for { + select { + case <-timer.C: + lastTick = time.Now() + + s.paintUpdate(timer, true) + + case <-pause: + <-s.unpauseCh + close(s.unpausedCh) + + case <-dataUpdate: + // if this is not a TTY: animate the spinner on the data update + s.paintUpdate(timer, termModeForceNoTTY(s.termMode)) + + case frequency := <-frequencyUpdate: + handleFrequencyUpdate(frequency, timer, lastTick) + + case _, ok := <-cancel: + defer close(done) + + timer.Stop() + + s.paintStop(ok) + + return + } + } +} + +func (s *Spinner) paintUpdate(timer *time.Timer, animate bool) { + s.mu.Lock() + + p := s.prefix + m := s.message + suf := s.suffix + mw := s.maxWidth + cFn := s.colorFn + d := s.frequency + index := s.index + + if animate { + s.index++ + + if s.index == len(s.chars) { + s.index = 0 + } + } else { + // for data updates use the last spinner char + index-- + + if index < 0 { + index = len(s.chars) - 1 + } + } + + c := s.chars[index] + + s.mu.Unlock() + + defer s.buffer.Reset() + + if termModeForceSmart(s.termMode) { + if err := erase(s.buffer); err != nil { + panic(fmt.Sprintf("failed to erase line: %v", err)) + } + + if s.cursorHidden { + if err := hideCursor(s.buffer); err != nil { + panic(fmt.Sprintf("failed to hide cursor: %v", err)) + } + } + + if _, err := paint(s.buffer, mw, c, p, m, suf, s.suffixAutoColon, s.colorAll, s.spinnerAtEnd, false, termModeForceNoTTY(s.termMode), cFn); err != nil { + panic(fmt.Sprintf("failed to paint line: %v", err)) + } + } else { + if err := s.eraseDumbTerm(s.buffer); err != nil { + panic(fmt.Sprintf("failed to erase line: %v", err)) + } + + n, err := paint(s.buffer, mw, c, p, m, suf, s.suffixAutoColon, false, s.spinnerAtEnd, false, termModeForceNoTTY(s.termMode), fmt.Sprintf) + if err != nil { + panic(fmt.Sprintf("failed to paint line: %v", err)) + } + + s.lastPrintLen = n + } + + if s.buffer.Len() > 0 { + if _, err := s.writer.Write(s.buffer.Bytes()); err != nil { + panic(fmt.Sprintf("failed to output buffer to writer: %v", err)) + } + } + + if animate { + timer.Reset(d) + } +} + +func (s *Spinner) paintStop(chanOk bool) { + var m string + var c character + var cFn func(format string, a ...interface{}) string + + s.mu.Lock() + + if chanOk { + c = s.stopChar + cFn = s.stopColorFn + m = s.stopMsg + } else { + c = s.stopFailChar + cFn = s.stopFailColorFn + m = s.stopFailMsg + } + + p := s.prefix + suf := s.suffix + mw := s.maxWidth + + s.mu.Unlock() + + defer s.buffer.Reset() + + if termModeForceSmart(s.termMode) { + if err := erase(s.buffer); err != nil { + panic(fmt.Sprintf("failed to erase line: %v", err)) + } + + if s.cursorHidden { + if err := unhideCursor(s.buffer); err != nil { + panic(fmt.Sprintf("failed to hide cursor: %v", err)) + } + } + + if c.Size > 0 || len(m) > 0 { + // paint the line with a newline as it's the final line + if _, err := paint(s.buffer, mw, c, p, m, suf, s.suffixAutoColon, s.colorAll, s.spinnerAtEnd, true, termModeForceNoTTY(s.termMode), cFn); err != nil { + panic(fmt.Sprintf("failed to paint line: %v", err)) + } + } + } else { + if err := s.eraseDumbTerm(s.buffer); err != nil { + panic(fmt.Sprintf("failed to erase line: %v", err)) + } + + if c.Size > 0 || len(m) > 0 { + if _, err := paint(s.buffer, mw, c, p, m, suf, s.suffixAutoColon, false, s.spinnerAtEnd, true, termModeForceNoTTY(s.termMode), fmt.Sprintf); err != nil { + panic(fmt.Sprintf("failed to paint line: %v", err)) + } + } + + s.lastPrintLen = 0 + } + + if s.buffer.Len() > 0 { + if _, err := s.writer.Write(s.buffer.Bytes()); err != nil { + panic(fmt.Sprintf("failed to output buffer to writer: %v", err)) + } + } +} + +// erase clears the line +func erase(w io.Writer) error { + _, err := fmt.Fprint(w, "\r\033[K\r") + return err +} + +// eraseDumbTerm clears the line on dumb terminals +func (s *Spinner) eraseDumbTerm(w io.Writer) error { + if termModeForceNoTTY(s.termMode) { + // non-TTY outputs use \n instead of line erasure, + // so return early + return nil + } + + clear := "\r" + strings.Repeat(" ", s.lastPrintLen) + "\r" + + _, err := fmt.Fprint(w, clear) + return err +} + +func hideCursor(w io.Writer) error { + _, err := fmt.Fprint(w, "\r\033[?25l\r") + return err +} + +func unhideCursor(w io.Writer) error { + _, err := fmt.Fprint(w, "\r\033[?25h\r") + return err +} + +// padChar pads the spinner character so suffix / message offset from left is +// consistent +func padChar(char character, maxWidth int) string { + padSize := maxWidth - char.Size + return char.Value + strings.Repeat(" ", padSize) +} + +// paint writes a single line to the w, using the provided character, message, +// and color function +func paint(w io.Writer, maxWidth int, char character, prefix, message, suffix string, suffixAutoColon, colorAll, spinnerAtEnd, finalPaint, notTTY bool, colorFn func(format string, a ...interface{}) string) (int, error) { + var output string + + switch char.Size { + case 0: + if colorAll { + output = colorFn(message) + break + } + + output = message + + default: + c := padChar(char, maxWidth) + + if spinnerAtEnd { + if colorAll { + output = colorFn("%s%s%s%s", message, prefix, c, suffix) + break + } + + output = fmt.Sprintf("%s%s%s%s", message, prefix, colorFn(c), suffix) + break + } + + if suffixAutoColon { // also implicitly !spinnerAtEnd + if len(strings.TrimSpace(suffix)) > 0 && len(message) > 0 && message != "\n" { + suffix += ": " + } + } + + if colorAll { + output = colorFn("%s%s%s%s", prefix, c, suffix, message) + break + } + + output = fmt.Sprintf("%s%s%s%s", prefix, colorFn(c), suffix, message) + } + + if finalPaint || notTTY { + output += "\n" + } + + return fmt.Fprint(w, output) +} + +// Frequency updates the frequency of the spinner being animated. +func (s *Spinner) Frequency(d time.Duration) error { + if d < 1 { + return errors.New("duration must be greater than 0") + } + + if termModeForceNoTTY(s.termMode) { + // when output target is not a TTY, we don't animate spinner + // so there is no need to update the frequency + return nil + } + + s.mu.Lock() + defer s.mu.Unlock() + + s.frequency = d + + // non-blocking notification + select { + case s.frequencyUpdateCh <- d: + default: + } + + return nil +} + +// Prefix updates the Prefix before the spinner character. +func (s *Spinner) Prefix(prefix string) { + s.mu.Lock() + defer s.mu.Unlock() + + s.prefix = prefix + + s.notifyDataChange() +} + +// Suffix updates the Suffix printed after the spinner character and before the +// message. It's recommended that this start with an empty space. +func (s *Spinner) Suffix(suffix string) { + s.mu.Lock() + defer s.mu.Unlock() + + s.suffix = suffix + + s.notifyDataChange() +} + +// Message updates the Message displayed after the suffix. +func (s *Spinner) Message(message string) { + s.mu.Lock() + defer s.mu.Unlock() + + s.message = message + + s.notifyDataChange() +} + +// Colors updates the github.com/fatih/colors for printing the spinner line. +// ColorAll config parameter controls whether only the spinner character is +// printed with these colors, or the whole line. +// +// StopColors() is the method to control the colors in the stop message. +func (s *Spinner) Colors(colors ...string) error { + colorFn, err := colorFunc(colors...) + if err != nil { + return fmt.Errorf("failed to build color function: %w", err) + } + + s.mu.Lock() + defer s.mu.Unlock() + + s.colorFn = colorFn + + s.notifyDataChange() + + return nil +} + +// StopMessage updates the Message used when Stop() is called. +func (s *Spinner) StopMessage(message string) { + s.mu.Lock() + defer s.mu.Unlock() + + s.stopMsg = message + + s.notifyDataChange() +} + +// StopColors updates the colors used for the stop message. See Colors() method +// documentation for more context. +// +// StopFailColors() is the method to control the colors in the failed stop +// message. +func (s *Spinner) StopColors(colors ...string) error { + colorFn, err := colorFunc(colors...) + if err != nil { + return fmt.Errorf("failed to build stop color function: %w", err) + } + + s.mu.Lock() + defer s.mu.Unlock() + + s.stopColorFn = colorFn + + s.notifyDataChange() + + return nil +} + +// StopCharacter sets the single "character" to use for the spinner when +// stopping. Recommended character is ✓. +func (s *Spinner) StopCharacter(char string) { + n := runewidth.StringWidth(char) + + s.mu.Lock() + defer s.mu.Unlock() + + s.stopChar = character{Value: char, Size: n} + + if n > s.maxWidth { + s.maxWidth = n + } + + s.notifyDataChange() +} + +// StopFailMessage updates the Message used when StopFail() is called. +func (s *Spinner) StopFailMessage(message string) { + s.mu.Lock() + defer s.mu.Unlock() + + s.stopFailMsg = message + + s.notifyDataChange() +} + +// StopFailColors updates the colors used for the StopFail message. See Colors() method +// documentation for more context. +func (s *Spinner) StopFailColors(colors ...string) error { + colorFn, err := colorFunc(colors...) + if err != nil { + return fmt.Errorf("failed to build stop fail color function: %w", err) + } + + s.mu.Lock() + defer s.mu.Unlock() + + s.stopFailColorFn = colorFn + + s.notifyDataChange() + + return nil +} + +// StopFailCharacter sets the single "character" to use for the spinner when +// stopping for a failure. Recommended character is ✗. +func (s *Spinner) StopFailCharacter(char string) { + n := runewidth.StringWidth(char) + + s.mu.Lock() + defer s.mu.Unlock() + + s.stopFailChar = character{Value: char, Size: n} + + if n > s.maxWidth { + s.maxWidth = n + } + + s.notifyDataChange() +} + +// CharSet updates the set of characters (strings) to use for the spinner. You +// can provide your own, or use one from the yacspin.CharSets variable. +// +// The character sets available in the CharSets variable are from the +// https://github.com/briandowns/spinner project. +func (s *Spinner) CharSet(cs []string) error { + if len(cs) == 0 { + return errors.New("failed to set character set: must provide at least one string") + } + + chars, mw := setToCharSlice(cs) + s.mu.Lock() + defer s.mu.Unlock() + + if n := s.stopChar.Size; n > mw { + mw = s.stopChar.Size + } + + if n := s.stopFailChar.Size; n > mw { + mw = n + } + + s.chars = chars + s.maxWidth = mw + s.index = 0 + + return nil +} + +// Reverse flips the character set order of the spinner characters. +func (s *Spinner) Reverse() { + s.mu.Lock() + defer s.mu.Unlock() + + for i, j := 0, len(s.chars)-1; i < j; { + s.chars[i], s.chars[j] = s.chars[j], s.chars[i] + i++ + j-- + } + + s.index = 0 +} + +func uint32Ptr(u uint32) *uint32 { return &u } diff --git a/vendor/modules.txt b/vendor/modules.txt index d4c89cfda..a45066b23 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1849,6 +1849,9 @@ github.com/tchap/go-patricia/v2/patricia ## explicit github.com/test-go/testify/mock github.com/test-go/testify/require +# github.com/theckman/yacspin v0.13.12 +## explicit; go 1.17 +github.com/theckman/yacspin # github.com/thejerf/suture/v4 v4.0.6 ## explicit; go 1.9 github.com/thejerf/suture/v4