mirror of
https://github.com/BishopFox/sliver.git
synced 2026-11-07 17:08:13 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8d59daded | ||
|
|
9195376993 | ||
|
|
a6de8e0304 | ||
|
|
310619526d | ||
|
|
1990777c4b | ||
|
|
9570556a87 | ||
|
|
b563919c19 | ||
|
|
f1858ee8f3 | ||
|
|
46a8313a33 |
No files matched your search
@@ -21,44 +21,24 @@ K := $(foreach exec,$(EXECUTABLES),\
|
||||
$(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH")))
|
||||
|
||||
#
|
||||
# Version Information
|
||||
# Build Information
|
||||
#
|
||||
GO_VERSION = $(shell $(GO) version)
|
||||
GO_MAJOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1)
|
||||
GO_MINOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
|
||||
MIN_SUPPORTED_GO_MAJOR_VERSION = 1
|
||||
MIN_SUPPORTED_GO_MINOR_VERSION = 24
|
||||
GO_VERSION_VALIDATION_ERR_MSG = Golang version is not supported, please update to at least $(MIN_SUPPORTED_GO_MAJOR_VERSION).$(MIN_SUPPORTED_GO_MINOR_VERSION)
|
||||
|
||||
VERSION ?= $(shell git describe --abbrev=0)
|
||||
COMPILED_AT = $(shell date +%s)
|
||||
RELEASES_URL ?= https://api.github.com/repos/BishopFox/sliver/releases
|
||||
ARMORY_PUBLIC_KEY ?= RWSBpxpRWDrD7Fe+VvRE3c2VEDC2NK80rlNCj+BX0gz44Xw07r6KQD9L
|
||||
ARMORY_REPO_URL ?= https://api.github.com/repos/sliverarmory/armory/releases
|
||||
VERSION_PKG = github.com/bishopfox/sliver/client/version
|
||||
CLIENT_ASSETS_PKG = github.com/bishopfox/sliver/client/assets
|
||||
|
||||
GIT_DIRTY = $(shell git diff --quiet|| echo 'Dirty')
|
||||
GIT_COMMIT = $(shell git rev-parse HEAD)
|
||||
|
||||
LDFLAGS = -ldflags "-s -w \
|
||||
-X $(VERSION_PKG).Version=$(VERSION) \
|
||||
-X \"$(VERSION_PKG).GoVersion=$(GO_VERSION)\" \
|
||||
-X $(VERSION_PKG).CompiledAt=$(COMPILED_AT) \
|
||||
-X $(VERSION_PKG).GithubReleasesURL=$(RELEASES_URL) \
|
||||
-X $(VERSION_PKG).GitCommit=$(GIT_COMMIT) \
|
||||
-X $(VERSION_PKG).GitDirty=$(GIT_DIRTY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
# Debug builds shouldn't be stripped (-s -w flags)
|
||||
LDFLAGS_DEBUG = -ldflags "-X $(VERSION_PKG).Version=$(VERSION) \
|
||||
-X \"$(VERSION_PKG).GoVersion=$(GO_VERSION)\" \
|
||||
-X $(VERSION_PKG).CompiledAt=$(COMPILED_AT) \
|
||||
-X $(VERSION_PKG).GithubReleasesURL=$(RELEASES_URL) \
|
||||
-X $(VERSION_PKG).GitCommit=$(GIT_COMMIT) \
|
||||
-X $(VERSION_PKG).GitDirty=$(GIT_DIRTY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
|
||||
SED_INPLACE := sed -i
|
||||
@@ -93,12 +73,6 @@ ifeq ($(MAKECMDGOALS), linux)
|
||||
# Redefine LDFLAGS to add the static part
|
||||
LDFLAGS = -ldflags "-s -w \
|
||||
-extldflags '-static' \
|
||||
-X $(VERSION_PKG).Version=$(VERSION) \
|
||||
-X \"$(VERSION_PKG).GoVersion=$(GO_VERSION)\" \
|
||||
-X $(VERSION_PKG).CompiledAt=$(COMPILED_AT) \
|
||||
-X $(VERSION_PKG).GithubReleasesURL=$(RELEASES_URL) \
|
||||
-X $(VERSION_PKG).GitCommit=$(GIT_COMMIT) \
|
||||
-X $(VERSION_PKG).GitDirty=$(GIT_DIRTY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
|
||||
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
|
||||
endif
|
||||
|
||||
+10
-24
@@ -23,7 +23,7 @@ import (
|
||||
"sort"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"gopkg.in/AlecAivazis/survey.v1"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
)
|
||||
|
||||
func selectConfig() *assets.ClientConfig {
|
||||
@@ -39,32 +39,18 @@ func selectConfig() *assets.ClientConfig {
|
||||
}
|
||||
}
|
||||
|
||||
answer := struct{ Config string }{}
|
||||
qs := getPromptForConfigs(configs)
|
||||
err := survey.Ask(qs, &answer)
|
||||
keys := make([]string, 0, len(configs))
|
||||
for key := range configs {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
selection := keys[0]
|
||||
err := forms.Select("Select a server:", keys, &selection)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
return configs[answer.Config]
|
||||
}
|
||||
|
||||
func getPromptForConfigs(configs map[string]*assets.ClientConfig) []*survey.Question {
|
||||
keys := []string{}
|
||||
for k := range configs {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
return []*survey.Question{
|
||||
{
|
||||
Name: "config",
|
||||
Prompt: &survey.Select{
|
||||
Message: "Select a server:",
|
||||
Options: keys,
|
||||
Default: keys[0],
|
||||
},
|
||||
},
|
||||
}
|
||||
return configs[selection]
|
||||
}
|
||||
@@ -24,9 +24,9 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -62,8 +62,7 @@ func installFromDir(aliasLocalPath string, con *console.SliverClient) {
|
||||
if _, err := os.Stat(installPath); !os.IsNotExist(err) {
|
||||
con.PrintInfof("Alias '%s' already exists", manifest.CommandName)
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "Overwrite current install?"}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
_ = forms.Confirm("Overwrite current install?", &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
@@ -122,8 +121,7 @@ func InstallFromFile(aliasGzFilePath string, aliasName string, promptToOverwrite
|
||||
if promptToOverwrite {
|
||||
con.PrintInfof("Alias '%s' already exists\n", manifest.CommandName)
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "Overwrite current install?"}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
_ = forms.Confirm("Overwrite current install?", &confirm)
|
||||
if !confirm {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -27,11 +27,11 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/client/packages"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
@@ -313,8 +313,7 @@ func runAliasCommand(cmd *cobra.Command, con *console.SliverClient, args []strin
|
||||
}
|
||||
con.PrintWarnf("%s", msgStr)
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "Do you want to continue?"}
|
||||
survey.AskOne(prompt, &confirm, nil)
|
||||
_ = forms.Confirm("Do you want to continue?", &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -24,9 +24,9 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -39,8 +39,7 @@ func AliasesRemoveCmd(cmd *cobra.Command, con *console.SliverClient, args []stri
|
||||
return
|
||||
}
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: fmt.Sprintf("Remove '%s' alias?", name)}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
_ = forms.Confirm(fmt.Sprintf("Remove '%s' alias?", name), &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ import (
|
||||
"path/filepath"
|
||||
"slices"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
@@ -35,6 +34,7 @@ import (
|
||||
"github.com/bishopfox/sliver/client/command/extensions"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/bishopfox/sliver/util/minisign"
|
||||
)
|
||||
@@ -95,12 +95,9 @@ func ArmoryInstallCmd(cmd *cobra.Command, con *console.SliverClient, args []stri
|
||||
if extCount == 1 {
|
||||
pluralExtensions = ""
|
||||
}
|
||||
prompt := &survey.Confirm{
|
||||
Message: fmt.Sprintf("Install %d alias%s and %d extension%s?",
|
||||
aliasCount, pluralAliases, extCount, pluralExtensions,
|
||||
),
|
||||
}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
forms.Confirm(fmt.Sprintf("Install %d alias%s and %d extension%s?",
|
||||
aliasCount, pluralAliases, extCount, pluralExtensions,
|
||||
), &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
@@ -296,11 +293,7 @@ func getPackageIDFromUser(name string, options map[string]string) string {
|
||||
// Add a cancel option
|
||||
optionKeys = append(optionKeys, doNotInstallOption)
|
||||
options[doNotInstallOption] = doNotInstallPackageName
|
||||
prompt := &survey.Select{
|
||||
Message: fmt.Sprintf("More than one package contains the command %s. Please choose an option from the list below:", name),
|
||||
Options: optionKeys,
|
||||
}
|
||||
survey.AskOne(prompt, &selectedPackageKey)
|
||||
forms.Select(fmt.Sprintf("More than one package contains the command %s. Please choose an option from the list below:", name), optionKeys, &selectedPackageKey)
|
||||
selectedPackageID := options[selectedPackageKey]
|
||||
|
||||
return selectedPackageID
|
||||
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -602,8 +602,7 @@ func ModifyArmoryCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
|
||||
if err != nil {
|
||||
con.PrintErrorf("could not modify armory: %s\n", err)
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "Would you like to revert the properties of the armory back to their previous values?"}
|
||||
survey.AskOne(prompt, &confirm, nil)
|
||||
_ = forms.Confirm("Would you like to revert the properties of the armory back to their previous values?", &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
} else {
|
||||
|
||||
@@ -25,7 +25,6 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
@@ -34,6 +33,7 @@ import (
|
||||
"github.com/bishopfox/sliver/client/command/extensions"
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
)
|
||||
|
||||
@@ -89,7 +89,7 @@ func ArmoryUpdateCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
|
||||
displayAvailableUpdates(con, updateKeys, aliasUpdates, extUpdates)
|
||||
selectedUpdates, err = getUpdatesFromUser(con, updateKeys)
|
||||
if err != nil {
|
||||
con.PrintErrorf(err.Error() + "\n")
|
||||
con.PrintErrorf("%s\n", err.Error())
|
||||
return
|
||||
}
|
||||
if len(selectedUpdates) == 0 {
|
||||
@@ -305,9 +305,7 @@ func getUpdatesFromUser(con *console.SliverClient, updateKeys []UpdateIdentifier
|
||||
var updateResponse string
|
||||
|
||||
con.Println("You can apply all, none, or some updates.\nTo apply some updates, specify the number of a single update, a range (1-3), or a combination of the two (1, 3-5, 7)\n")
|
||||
err := survey.AskOne(&survey.Input{
|
||||
Message: "Which updates would you like to apply? [A]ll, [N]one, or some:",
|
||||
}, &updateResponse)
|
||||
err := forms.Input("Which updates would you like to apply? [A]ll, [N]one, or some:", &updateResponse)
|
||||
if err != nil {
|
||||
// We do not need to set selectionError because an error here means the same thing as responding with "none"
|
||||
return
|
||||
|
||||
@@ -26,9 +26,8 @@ import (
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
)
|
||||
@@ -83,12 +82,8 @@ func SelectBeacon(con *console.SliverClient) (*clientpb.Beacon, error) {
|
||||
|
||||
options := strings.Split(outputBuf.String(), "\n")
|
||||
options = options[:len(options)-1] // Remove the last empty option
|
||||
prompt := &survey.Select{
|
||||
Message: "Select a beacon:",
|
||||
Options: options,
|
||||
}
|
||||
selected := ""
|
||||
survey.AskOne(prompt, &selected)
|
||||
_ = forms.Select("Select a beacon:", options, &selected)
|
||||
if selected == "" {
|
||||
return nil, ErrNoSelection
|
||||
}
|
||||
|
||||
@@ -21,10 +21,10 @@ package beacons
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
)
|
||||
@@ -71,8 +71,7 @@ func BeaconsPruneCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
|
||||
}
|
||||
con.Println()
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "Prune these beacons?"}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
_ = forms.Confirm("Prune these beacons?", &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/exp/slices"
|
||||
@@ -38,6 +37,7 @@ import (
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
)
|
||||
@@ -547,11 +547,7 @@ func selectC2Profile(c2profiles []*clientpb.HTTPC2Config) string {
|
||||
choices = append(choices, c2profile.Name)
|
||||
}
|
||||
|
||||
prompt := &survey.Select{
|
||||
Message: "Select a c2 profile",
|
||||
Options: choices,
|
||||
}
|
||||
survey.AskOne(prompt, &c2profile, nil)
|
||||
_ = forms.Select("Select a c2 profile", choices, &c2profile)
|
||||
|
||||
return c2profile
|
||||
}
|
||||
|
||||
@@ -21,10 +21,10 @@ package clean
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/command/flags"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
@@ -35,8 +35,7 @@ import (
|
||||
func CleanCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
con.Printf("This command will kill and remove all sessions, beacons and profiles \n")
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "Are you sure you want to destroy everything?"}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
_ = forms.Confirm("Are you sure you want to destroy everything?", &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
)
|
||||
@@ -55,11 +55,7 @@ func SelectCredential(plaintext bool, hashType clientpb.HashType, con *console.S
|
||||
}
|
||||
|
||||
selected := ""
|
||||
prompt := &survey.Select{
|
||||
Message: "Select a credential:",
|
||||
Options: options,
|
||||
}
|
||||
err = survey.AskOne(prompt, &selected)
|
||||
err = forms.Select("Select a credential:", options, &selected)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -27,9 +27,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/core"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/client/overlord"
|
||||
"github.com/bishopfox/sliver/client/tcpproxy"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
@@ -123,7 +123,7 @@ func avadaKedavraChrome(session *clientpb.Session, cmd *cobra.Command, con *cons
|
||||
con.PrintWarnf("Sliver will attempt to restore the user's session, however %sDATA LOSS MAY OCCUR!%s\n", console.Bold, console.Normal)
|
||||
con.Printf("\n")
|
||||
confirm := false
|
||||
err = survey.AskOne(&survey.Confirm{Message: "Kill and restore existing Chrome process?"}, &confirm)
|
||||
err = forms.Confirm("Kill and restore existing Chrome process?", &confirm)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return nil
|
||||
|
||||
@@ -26,9 +26,9 @@ import (
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/core"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/client/overlord"
|
||||
"github.com/reeflective/readline"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -71,12 +71,8 @@ func selectDebugTarget(targets []overlord.ChromeDebugTarget, con *console.Sliver
|
||||
table.Flush()
|
||||
options := strings.Split(outputBuf.String(), "\n")
|
||||
options = options[:len(options)-1] // Remove the last empty option
|
||||
prompt := &survey.Select{
|
||||
Message: "Select a debug target:",
|
||||
Options: options,
|
||||
}
|
||||
selected := ""
|
||||
err := survey.AskOne(prompt, &selected)
|
||||
err := forms.Select("Select a debug target:", options, &selected)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return nil
|
||||
|
||||
@@ -23,9 +23,9 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/core"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/client/overlord"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
@@ -106,7 +106,7 @@ func avadaKedavraEdge(session *clientpb.Session, cmd *cobra.Command, con *consol
|
||||
con.PrintWarnf("Sliver will attempt to restore the user's session, however %sDATA LOSS MAY OCCUR!%s\n", console.Bold, console.Normal)
|
||||
con.Printf("\n")
|
||||
confirm := false
|
||||
err = survey.AskOne(&survey.Confirm{Message: "Kill and restore existing Edge process?"}, &confirm)
|
||||
err = forms.Confirm("Kill and restore existing Edge process?", &confirm)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return nil
|
||||
|
||||
@@ -25,9 +25,9 @@ import (
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/core"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/client/overlord"
|
||||
"github.com/bishopfox/sliver/client/tcpproxy"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
@@ -88,7 +88,7 @@ func avadaKedavraElectron(electronExe string, session *clientpb.Session, cmd *co
|
||||
con.PrintWarnf("%sDATA LOSS MAY OCCUR!%s\n", console.Bold, console.Normal)
|
||||
con.Printf("\n")
|
||||
confirm := false
|
||||
err = survey.AskOne(&survey.Confirm{Message: "Kill and restart the process?"}, &confirm)
|
||||
err = forms.Confirm("Kill and restart the process?", &confirm)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return nil
|
||||
|
||||
@@ -22,9 +22,9 @@ import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/core"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -44,7 +44,7 @@ func CursedRmCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
core.CloseCursedProcessesByBindPort(session.ID, bindPort)
|
||||
if kill {
|
||||
confirm := false
|
||||
err := survey.AskOne(&survey.Confirm{Message: "Kill the cursed process?"}, &confirm)
|
||||
err := forms.Confirm("Kill the cursed process?", &confirm)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s", err)
|
||||
return
|
||||
|
||||
@@ -25,10 +25,10 @@ import (
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/core"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -95,12 +95,8 @@ func selectCursedProcess(con *console.SliverClient) *core.CursedProcess {
|
||||
table.Flush()
|
||||
options := strings.Split(outputBuf.String(), "\n")
|
||||
options = options[:len(options)-1] // Remove the last empty option
|
||||
prompt := &survey.Select{
|
||||
Message: "Select a curse:",
|
||||
Options: options,
|
||||
}
|
||||
selected := ""
|
||||
err := survey.AskOne(prompt, &selected)
|
||||
err := forms.Select("Select a curse:", options, &selected)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return nil
|
||||
|
||||
@@ -24,8 +24,8 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -82,8 +82,7 @@ func ExecuteAssemblyCmd(cmd *cobra.Command, con *console.SliverClient, args []st
|
||||
if len(assemblyArgsStr) > 256 && !inProcess {
|
||||
con.PrintWarnf(" Injected .NET assembly arguments are limited to 256 characters when using the default fork/exec model.\nConsider using the --in-process flag to execute the .NET assembly in-process and work around this limitation.\n")
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "Do you want to continue?"}
|
||||
survey.AskOne(prompt, &confirm, nil)
|
||||
_ = forms.Confirm("Do you want to continue?", &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -145,8 +145,7 @@ func tryCredsFromLoot(con *console.SliverClient) (string, string, []byte) {
|
||||
privKey []byte
|
||||
)
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "No credentials provided, use from loot?"}
|
||||
survey.AskOne(prompt, &confirm, nil)
|
||||
_ = forms.Confirm("No credentials provided, use from loot?", &confirm)
|
||||
if confirm {
|
||||
// cred, err := loot.SelectCredentials(con)
|
||||
// if err != nil {
|
||||
|
||||
@@ -23,9 +23,9 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -45,8 +45,7 @@ func ExitCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
if 0 < len(sessions.Sessions) || 0 < len(beacons.Beacons) {
|
||||
con.Printf("There are %d active sessions and %d active beacons.\n", len(sessions.Sessions), len(beacons.Beacons))
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "Are you sure you want to exit?"}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
_ = forms.Confirm("Are you sure you want to exit?", &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -24,9 +24,9 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -92,8 +92,7 @@ func InstallFromDir(extLocalPath string, promptToOverwrite bool, con *console.Sl
|
||||
if promptToOverwrite {
|
||||
con.PrintInfof("Extension '%s' already exists", manifestF.Name)
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "Overwrite current install?"}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
_ = forms.Confirm("Overwrite current install?", &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -33,12 +33,12 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/client/core"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/client/packages"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
@@ -171,8 +171,7 @@ func ExtensionLoadCmd(cmd *cobra.Command, con *console.SliverClient, args []stri
|
||||
if CmdExists(extCmd.CommandName, sliverMenu.Command) {
|
||||
con.PrintErrorf("%s command already exists\n", extCmd.CommandName)
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "Overwrite current command?"}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
_ = forms.Confirm("Overwrite current command?", &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -25,9 +25,9 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -40,8 +40,7 @@ func ExtensionsRemoveCmd(cmd *cobra.Command, con *console.SliverClient, args []s
|
||||
return
|
||||
}
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: fmt.Sprintf("Remove '%s' extension?", name)}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
_ = forms.Confirm(fmt.Sprintf("Remove '%s' extension?", name), &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
@@ -104,8 +103,7 @@ func RemoveExtensionByManifestName(manifestName string, con *console.SliverClien
|
||||
// If path is outside extensions directory, prompt for confirmation
|
||||
if !strings.HasPrefix(extPath, assets.GetExtensionsDir()) {
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: fmt.Sprintf("Remove '%s' extension directory from filesystem?", manifestName)}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
_ = forms.Confirm(fmt.Sprintf("Remove '%s' extension directory from filesystem?", manifestName), &confirm)
|
||||
if !confirm {
|
||||
// Skip the forceRemoveAll but continue with the rest
|
||||
delete(loadedManifests, manifestName)
|
||||
|
||||
@@ -30,12 +30,12 @@ import (
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/loot"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/bishopfox/sliver/util/encoders"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"gopkg.in/AlecAivazis/survey.v1"
|
||||
)
|
||||
|
||||
func DownloadCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
@@ -189,8 +189,7 @@ func HandleDownloadResponse(download *sliverpb.Download, cmd *cobra.Command, arg
|
||||
|
||||
if _, err := os.Stat(dst); err == nil {
|
||||
overwrite := false
|
||||
prompt := &survey.Confirm{Message: "Overwrite local file?"}
|
||||
survey.AskOne(prompt, &overwrite, nil)
|
||||
_ = forms.Confirm("Overwrite local file?", &overwrite)
|
||||
if !overwrite {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package generate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -17,6 +21,21 @@ var (
|
||||
|
||||
// GenerateBeaconCmd - The main command used to generate implant binaries
|
||||
func GenerateBeaconCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
if shouldRunGenerateBeaconForm(cmd, con, args) {
|
||||
result, err := forms.GenerateBeaconForm()
|
||||
if err != nil {
|
||||
if errors.Is(err, forms.ErrUserAborted) {
|
||||
return
|
||||
}
|
||||
con.PrintErrorf("Generate beacon form failed: %s\n", err)
|
||||
return
|
||||
}
|
||||
if err := applyGenerateBeaconForm(cmd, result); err != nil {
|
||||
con.PrintErrorf("Failed to apply generate beacon form values: %s\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
name, config := parseCompileFlags(cmd, con)
|
||||
if config == nil {
|
||||
return
|
||||
@@ -67,3 +86,99 @@ func parseBeaconFlags(cmd *cobra.Command, config *clientpb.ImplantConfig) error
|
||||
config.BeaconJitter = int64(time.Duration(beaconJitter) * time.Second)
|
||||
return nil
|
||||
}
|
||||
|
||||
func shouldRunGenerateBeaconForm(cmd *cobra.Command, con *console.SliverClient, args []string) bool {
|
||||
if con == nil || con.IsCLI {
|
||||
return false
|
||||
}
|
||||
if len(args) != 0 {
|
||||
return false
|
||||
}
|
||||
return cmd.Flags().NFlag() == 0
|
||||
}
|
||||
|
||||
func applyGenerateBeaconForm(cmd *cobra.Command, result *forms.GenerateBeaconFormResult) error {
|
||||
if err := cmd.Flags().Set("os", result.OS); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmd.Flags().Set("arch", result.Arch); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmd.Flags().Set("format", result.Format); err != nil {
|
||||
return err
|
||||
}
|
||||
name := strings.TrimSpace(result.Name)
|
||||
if name != "" {
|
||||
if err := cmd.Flags().Set("name", name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
save := strings.TrimSpace(result.Save)
|
||||
if save != "" {
|
||||
if err := cmd.Flags().Set("save", save); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
c2Value := strings.TrimSpace(result.C2Value)
|
||||
switch result.C2Type {
|
||||
case "mtls":
|
||||
if err := cmd.Flags().Set("mtls", c2Value); err != nil {
|
||||
return err
|
||||
}
|
||||
case "wg":
|
||||
if err := cmd.Flags().Set("wg", c2Value); err != nil {
|
||||
return err
|
||||
}
|
||||
case "http":
|
||||
if err := cmd.Flags().Set("http", c2Value); err != nil {
|
||||
return err
|
||||
}
|
||||
case "dns":
|
||||
if err := cmd.Flags().Set("dns", c2Value); err != nil {
|
||||
return err
|
||||
}
|
||||
case "named-pipe":
|
||||
if err := cmd.Flags().Set("named-pipe", c2Value); err != nil {
|
||||
return err
|
||||
}
|
||||
case "tcp-pivot":
|
||||
if err := cmd.Flags().Set("tcp-pivot", c2Value); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return errors.New("unsupported C2 transport selection")
|
||||
}
|
||||
|
||||
if err := setOptionalInt64Flag(cmd, "days", result.Days); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := setOptionalInt64Flag(cmd, "hours", result.Hours); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := setOptionalInt64Flag(cmd, "minutes", result.Minutes); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := setOptionalInt64Flag(cmd, "seconds", result.Seconds); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := setOptionalInt64Flag(cmd, "jitter", result.Jitter); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setOptionalInt64Flag(cmd *cobra.Command, name, value string) error {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" {
|
||||
return nil
|
||||
}
|
||||
parsed, err := strconv.ParseInt(trimmed, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if parsed < 0 {
|
||||
return fmt.Errorf("%s must be 0 or greater", name)
|
||||
}
|
||||
return cmd.Flags().Set(name, strconv.FormatInt(parsed, 10))
|
||||
}
|
||||
@@ -32,7 +32,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
@@ -202,13 +201,10 @@ func saveLocation(save, DefaultName string, con *console.SliverClient) (string,
|
||||
save = expandPath(save)
|
||||
fi, err := os.Stat(save)
|
||||
if os.IsNotExist(err) {
|
||||
con.Printf("%s does not exist\n", save)
|
||||
if strings.HasSuffix(save, "/") {
|
||||
con.Printf("%s is dir\n", save)
|
||||
os.MkdirAll(save, 0o700)
|
||||
saveTo, _ = filepath.Abs(filepath.Join(saveTo, DefaultName))
|
||||
} else {
|
||||
con.Printf("%s is not dir\n", save)
|
||||
saveDir := filepath.Dir(save)
|
||||
_, err := os.Stat(saveTo)
|
||||
if os.IsNotExist(err) {
|
||||
@@ -220,10 +216,8 @@ func saveLocation(save, DefaultName string, con *console.SliverClient) (string,
|
||||
if fi.IsDir() {
|
||||
saveTo, _ = filepath.Abs(filepath.Join(save, DefaultName))
|
||||
} else {
|
||||
con.PrintInfof("%s is not dir\n", save)
|
||||
prompt := &survey.Confirm{Message: "Overwrite existing file?"}
|
||||
var confirm bool
|
||||
survey.AskOne(prompt, &confirm)
|
||||
_ = forms.Confirm("Overwrite existing file?", &confirm)
|
||||
if !confirm {
|
||||
return "", errors.New("file already exists")
|
||||
}
|
||||
@@ -534,9 +528,8 @@ func getTargets(targetOS string, targetArch string, con *console.SliverClient) (
|
||||
console.Bold, target, console.Normal,
|
||||
)
|
||||
con.Printf("⚠️ Generic implants do not support all commands/features.\n")
|
||||
prompt := &survey.Confirm{Message: "Attempt to build generic implant?"}
|
||||
var confirm bool
|
||||
survey.AskOne(prompt, &confirm)
|
||||
_ = forms.Confirm("Attempt to build generic implant?", &confirm)
|
||||
if !confirm {
|
||||
return "", ""
|
||||
}
|
||||
@@ -806,11 +799,8 @@ func ParseNamedPipec2(args string) ([]*clientpb.ImplantC2, error) {
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(uri.Path, "/pipe/") {
|
||||
prompt := &survey.Confirm{
|
||||
Message: fmt.Sprintf("Named pipe '%s' is missing the 'pipe' path prefix\nContinue anyways?", uri),
|
||||
}
|
||||
var confirm bool
|
||||
survey.AskOne(prompt, &confirm)
|
||||
_ = forms.Confirm(fmt.Sprintf("Named pipe '%s' is missing the 'pipe' path prefix\nContinue anyways?", uri), &confirm)
|
||||
if !confirm {
|
||||
return nil, fmt.Errorf("invalid namedpipe path: %s", uri.Path)
|
||||
}
|
||||
@@ -1117,8 +1107,7 @@ func warnMissingCrossCompiler(format clientpb.OutputFormat, targetOS string, tar
|
||||
con.PrintWarnf("For more information please read %s\n", crossCompilerInfoURL)
|
||||
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "Try to compile anyways (will likely fail)?"}
|
||||
survey.AskOne(prompt, &confirm, nil)
|
||||
_ = forms.Confirm("Try to compile anyways (will likely fail)?", &confirm)
|
||||
return confirm
|
||||
}
|
||||
|
||||
@@ -1154,11 +1143,7 @@ func selectExternalBuilder(builders []*clientpb.Builder, _ *console.SliverClient
|
||||
choices = append(choices, builder.Name)
|
||||
}
|
||||
choice := ""
|
||||
prompt := &survey.Select{
|
||||
Message: "Select an external builder:",
|
||||
Options: choices,
|
||||
}
|
||||
survey.AskOne(prompt, &choice, nil)
|
||||
_ = forms.Select("Select an external builder:", choices, &choice)
|
||||
for _, builder := range builders {
|
||||
if builder.Name == choice {
|
||||
return builder, nil
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -24,8 +24,7 @@ func ImplantsRmCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
|
||||
return
|
||||
}
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: fmt.Sprintf("Remove '%s' build?", name)}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
_ = forms.Confirm(fmt.Sprintf("Remove '%s' build?", name), &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -21,10 +21,10 @@ package generate
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
)
|
||||
@@ -42,12 +42,8 @@ func ImplantsStageCmd(cmd *cobra.Command, con *console.SliverClient, args []stri
|
||||
options = append(options, name)
|
||||
}
|
||||
|
||||
prompt := &survey.MultiSelect{
|
||||
Message: "Select sessions and beacons to expose:",
|
||||
Options: options,
|
||||
}
|
||||
selected := []string{}
|
||||
survey.AskOne(prompt, &selected)
|
||||
_ = forms.MultiSelect("Select sessions and beacons to expose:", options, &selected)
|
||||
|
||||
_, err = con.Rpc.StageImplantBuild(context.Background(), &clientpb.ImplantStageReq{Build: selected})
|
||||
if err != nil {
|
||||
|
||||
@@ -19,8 +19,11 @@ package generate
|
||||
*/
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -28,7 +31,22 @@ import (
|
||||
// ProfilesNewCmd - Create a new implant profile.
|
||||
func ProfilesNewCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
var name string
|
||||
if len(args) > 0 {
|
||||
if shouldRunGenerateProfilesNewForm(cmd, con, args) {
|
||||
result, err := forms.GenerateProfilesNewForm()
|
||||
if err != nil {
|
||||
if errors.Is(err, forms.ErrUserAborted) {
|
||||
return
|
||||
}
|
||||
con.PrintErrorf("Profiles new form failed: %s\n", err)
|
||||
return
|
||||
}
|
||||
profileName, err := applyGenerateProfilesNewForm(cmd, result)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to apply profiles new form values: %s\n", err)
|
||||
return
|
||||
}
|
||||
name = profileName
|
||||
} else if len(args) > 0 {
|
||||
name = args[0]
|
||||
}
|
||||
// name := ctx.Args.String("name")
|
||||
@@ -51,7 +69,22 @@ func ProfilesNewCmd(cmd *cobra.Command, con *console.SliverClient, args []string
|
||||
// ProfilesNewBeaconCmd - Create a new beacon profile.
|
||||
func ProfilesNewBeaconCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
var name string
|
||||
if len(args) > 0 {
|
||||
if shouldRunGenerateProfilesNewBeaconForm(cmd, con, args) {
|
||||
result, err := forms.GenerateProfilesNewBeaconForm()
|
||||
if err != nil {
|
||||
if errors.Is(err, forms.ErrUserAborted) {
|
||||
return
|
||||
}
|
||||
con.PrintErrorf("Profiles new beacon form failed: %s\n", err)
|
||||
return
|
||||
}
|
||||
profileName, err := applyGenerateProfilesNewBeaconForm(cmd, result)
|
||||
if err != nil {
|
||||
con.PrintErrorf("Failed to apply profiles new beacon form values: %s\n", err)
|
||||
return
|
||||
}
|
||||
name = profileName
|
||||
} else if len(args) > 0 {
|
||||
name = args[0]
|
||||
}
|
||||
// name := ctx.Args.String("name")
|
||||
@@ -80,3 +113,151 @@ func ProfilesNewBeaconCmd(cmd *cobra.Command, con *console.SliverClient, args []
|
||||
con.PrintInfof("Saved new implant profile (beacon) %s\n", resp.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func shouldRunGenerateProfilesNewForm(cmd *cobra.Command, con *console.SliverClient, args []string) bool {
|
||||
if con == nil || con.IsCLI {
|
||||
return false
|
||||
}
|
||||
if len(args) != 0 {
|
||||
return false
|
||||
}
|
||||
return cmd.Flags().NFlag() == 0
|
||||
}
|
||||
|
||||
func applyGenerateProfilesNewForm(cmd *cobra.Command, result *forms.GenerateProfilesNewFormResult) (string, error) {
|
||||
if err := cmd.Flags().Set("os", result.OS); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := cmd.Flags().Set("arch", result.Arch); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := cmd.Flags().Set("format", result.Format); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
profileName := strings.TrimSpace(result.ProfileName)
|
||||
if profileName == "" {
|
||||
return "", errors.New("profile name required")
|
||||
}
|
||||
|
||||
implantName := strings.TrimSpace(result.Name)
|
||||
if implantName != "" {
|
||||
if err := cmd.Flags().Set("name", implantName); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
c2Value := strings.TrimSpace(result.C2Value)
|
||||
switch result.C2Type {
|
||||
case "mtls":
|
||||
if err := cmd.Flags().Set("mtls", c2Value); err != nil {
|
||||
return "", err
|
||||
}
|
||||
case "wg":
|
||||
if err := cmd.Flags().Set("wg", c2Value); err != nil {
|
||||
return "", err
|
||||
}
|
||||
case "http":
|
||||
if err := cmd.Flags().Set("http", c2Value); err != nil {
|
||||
return "", err
|
||||
}
|
||||
case "dns":
|
||||
if err := cmd.Flags().Set("dns", c2Value); err != nil {
|
||||
return "", err
|
||||
}
|
||||
case "named-pipe":
|
||||
if err := cmd.Flags().Set("named-pipe", c2Value); err != nil {
|
||||
return "", err
|
||||
}
|
||||
case "tcp-pivot":
|
||||
if err := cmd.Flags().Set("tcp-pivot", c2Value); err != nil {
|
||||
return "", err
|
||||
}
|
||||
default:
|
||||
return "", errors.New("unsupported C2 transport selection")
|
||||
}
|
||||
|
||||
return profileName, nil
|
||||
}
|
||||
|
||||
func shouldRunGenerateProfilesNewBeaconForm(cmd *cobra.Command, con *console.SliverClient, args []string) bool {
|
||||
if con == nil || con.IsCLI {
|
||||
return false
|
||||
}
|
||||
if len(args) != 0 {
|
||||
return false
|
||||
}
|
||||
return cmd.Flags().NFlag() == 0
|
||||
}
|
||||
|
||||
func applyGenerateProfilesNewBeaconForm(cmd *cobra.Command, result *forms.GenerateProfilesNewBeaconFormResult) (string, error) {
|
||||
if err := cmd.Flags().Set("os", result.OS); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := cmd.Flags().Set("arch", result.Arch); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := cmd.Flags().Set("format", result.Format); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
profileName := strings.TrimSpace(result.ProfileName)
|
||||
if profileName == "" {
|
||||
return "", errors.New("profile name required")
|
||||
}
|
||||
|
||||
implantName := strings.TrimSpace(result.Name)
|
||||
if implantName != "" {
|
||||
if err := cmd.Flags().Set("name", implantName); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
c2Value := strings.TrimSpace(result.C2Value)
|
||||
switch result.C2Type {
|
||||
case "mtls":
|
||||
if err := cmd.Flags().Set("mtls", c2Value); err != nil {
|
||||
return "", err
|
||||
}
|
||||
case "wg":
|
||||
if err := cmd.Flags().Set("wg", c2Value); err != nil {
|
||||
return "", err
|
||||
}
|
||||
case "http":
|
||||
if err := cmd.Flags().Set("http", c2Value); err != nil {
|
||||
return "", err
|
||||
}
|
||||
case "dns":
|
||||
if err := cmd.Flags().Set("dns", c2Value); err != nil {
|
||||
return "", err
|
||||
}
|
||||
case "named-pipe":
|
||||
if err := cmd.Flags().Set("named-pipe", c2Value); err != nil {
|
||||
return "", err
|
||||
}
|
||||
case "tcp-pivot":
|
||||
if err := cmd.Flags().Set("tcp-pivot", c2Value); err != nil {
|
||||
return "", err
|
||||
}
|
||||
default:
|
||||
return "", errors.New("unsupported C2 transport selection")
|
||||
}
|
||||
|
||||
if err := setOptionalInt64Flag(cmd, "days", result.Days); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := setOptionalInt64Flag(cmd, "hours", result.Hours); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := setOptionalInt64Flag(cmd, "minutes", result.Minutes); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := setOptionalInt64Flag(cmd, "seconds", result.Seconds); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := setOptionalInt64Flag(cmd, "jitter", result.Jitter); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return profileName, nil
|
||||
}
|
||||
@@ -22,8 +22,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -45,8 +45,7 @@ func ProfilesRmCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
|
||||
return
|
||||
}
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: fmt.Sprintf("Remove '%s' profile?", name)}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
_ = forms.Confirm(fmt.Sprintf("Remove '%s' profile?", name), &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -26,10 +26,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
@@ -140,10 +140,7 @@ func TrafficEncodersAddCmd(cmd *cobra.Command, con *console.SliverClient, args [
|
||||
// saveFailedSample - Save the sample the encoder failed to properly encode/decode.
|
||||
func saveFailedSample(encoderName string, test *clientpb.TrafficEncoderTest) {
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{
|
||||
Message: fmt.Sprintf("Failed to add traffic encoder %s, save failed sample to disk?", encoderName),
|
||||
}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
_ = forms.Confirm(fmt.Sprintf("Failed to add traffic encoder %s, save failed sample to disk?", encoderName), &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
@@ -286,10 +283,6 @@ func SelectTrafficEncoder(con *console.SliverClient) string {
|
||||
}
|
||||
sort.Strings(encoderNames)
|
||||
var selectedEncoder string
|
||||
prompt := &survey.Select{
|
||||
Message: "Select a traffic encoder:",
|
||||
Options: encoderNames,
|
||||
}
|
||||
survey.AskOne(prompt, &selectedEncoder)
|
||||
_ = forms.Select("Select a traffic encoder:", encoderNames, &selectedEncoder)
|
||||
return selectedEncoder
|
||||
}
|
||||
@@ -25,8 +25,8 @@ import (
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -86,12 +86,8 @@ func SelectHostIOC(host *clientpb.Host, con *console.SliverClient) (*clientpb.IO
|
||||
|
||||
options := strings.Split(outputBuf.String(), "\n")
|
||||
options = options[:len(options)-1] // Remove the last empty option
|
||||
prompt := &survey.Select{
|
||||
Message: "Select an IOC:",
|
||||
Options: options,
|
||||
}
|
||||
selected := ""
|
||||
survey.AskOne(prompt, &selected)
|
||||
_ = forms.Select("Select an IOC:", options, &selected)
|
||||
if selected == "" {
|
||||
return nil, ErrNoSelection
|
||||
}
|
||||
|
||||
@@ -28,9 +28,9 @@ import (
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
@@ -172,12 +172,8 @@ func SelectHost(con *console.SliverClient) (*clientpb.Host, error) {
|
||||
|
||||
options := strings.Split(outputBuf.String(), "\n")
|
||||
options = options[:len(options)-1] // Remove the last empty option
|
||||
prompt := &survey.Select{
|
||||
Message: "Select a host:",
|
||||
Options: options,
|
||||
}
|
||||
selected := ""
|
||||
survey.AskOne(prompt, &selected)
|
||||
_ = forms.Select("Select a host:", options, &selected)
|
||||
if selected == "" {
|
||||
return nil, ErrNoSelection
|
||||
}
|
||||
|
||||
@@ -22,11 +22,11 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/core"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
@@ -39,7 +39,7 @@ func KillCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
confirm := false
|
||||
con.PrintWarnf("WARNING: This will kill the remote implant process\n\n")
|
||||
if session != nil {
|
||||
survey.AskOne(&survey.Confirm{Message: "Kill the active session?"}, &confirm, nil)
|
||||
_ = forms.Confirm("Kill the active session?", &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
@@ -52,7 +52,7 @@ func KillCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
con.ActiveTarget.Background()
|
||||
return
|
||||
} else if beacon != nil {
|
||||
survey.AskOne(&survey.Confirm{Message: "Kill the active beacon?"}, &confirm, nil)
|
||||
_ = forms.Confirm("Kill the active beacon?", &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -31,9 +31,9 @@ import (
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
@@ -106,11 +106,7 @@ func SelectLoot(cmd *cobra.Command, rpc rpcpb.SliverRPCClient) (*clientpb.Loot,
|
||||
}
|
||||
|
||||
selected := ""
|
||||
prompt := &survey.Select{
|
||||
Message: "Select a piece of loot:",
|
||||
Options: options,
|
||||
}
|
||||
err = survey.AskOne(prompt, &selected)
|
||||
err = forms.Select("Select a piece of loot:", options, &selected)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -26,12 +26,12 @@ import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
@@ -111,8 +111,7 @@ func saveLootToDisk(cmd *cobra.Command, loot *clientpb.Loot) (string, error) {
|
||||
}
|
||||
if _, err := os.Stat(saveTo); err == nil {
|
||||
overwrite := false
|
||||
prompt := &survey.Confirm{Message: "Overwrite local file?"}
|
||||
survey.AskOne(prompt, &overwrite, nil)
|
||||
_ = forms.Confirm("Overwrite local file?", &overwrite)
|
||||
if !overwrite {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
@@ -21,10 +21,10 @@ package loot
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
)
|
||||
|
||||
@@ -37,8 +37,7 @@ func LootRenameCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
|
||||
}
|
||||
oldName := loot.Name
|
||||
newName := ""
|
||||
prompt := &survey.Input{Message: "Enter new name: "}
|
||||
survey.AskOne(prompt, &newName)
|
||||
_ = forms.Input("Enter new name:", &newName)
|
||||
|
||||
loot, err = con.Rpc.LootUpdate(context.Background(), &clientpb.Loot{
|
||||
ID: loot.ID,
|
||||
|
||||
@@ -22,9 +22,9 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
@@ -121,11 +121,7 @@ func selectWatchtowerConfig(configs *clientpb.MonitoringProviders) (*clientpb.Mo
|
||||
}
|
||||
|
||||
selected := ""
|
||||
prompt := &survey.Select{
|
||||
Message: "Select a configuration:",
|
||||
Options: options,
|
||||
}
|
||||
err := survey.AskOne(prompt, &selected)
|
||||
err := forms.Select("Select a configuration:", options, &selected)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ import (
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/rsteube/carapace"
|
||||
)
|
||||
@@ -49,11 +49,7 @@ func SelectPivotListener(listeners []*sliverpb.PivotListener, con *console.Slive
|
||||
}
|
||||
|
||||
selected := ""
|
||||
prompt := &survey.Select{
|
||||
Message: "Select a beacon task:",
|
||||
Options: options,
|
||||
}
|
||||
err := survey.AskOne(prompt, &selected)
|
||||
err := forms.Select("Select a beacon task:", options, &selected)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ package reaction
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -33,8 +33,7 @@ func ReactionReloadCmd(cmd *cobra.Command, con *console.SliverClient, args []str
|
||||
return
|
||||
}
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "Reload reactions from disk?"}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
_ = forms.Confirm("Reload reactions from disk?", &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -22,9 +22,9 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/core"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -33,8 +33,7 @@ func ReactionSaveCmd(cmd *cobra.Command, con *console.SliverClient, args []strin
|
||||
reactionPath := GetReactionFilePath()
|
||||
if _, err := os.Stat(reactionPath); !os.IsNotExist(err) {
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: fmt.Sprintf("Overwrite reactions (%s) on disk?", reactionPath)}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
_ = forms.Confirm(fmt.Sprintf("Overwrite reactions (%s) on disk?", reactionPath), &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -22,9 +22,9 @@ import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/core"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -77,12 +77,8 @@ func getEventType(cmd *cobra.Command, con *console.SliverClient) (string, error)
|
||||
}
|
||||
|
||||
func selectEventType(con *console.SliverClient) (string, error) {
|
||||
prompt := &survey.Select{
|
||||
Message: "Select an event:",
|
||||
Options: core.ReactableEvents,
|
||||
}
|
||||
selection := ""
|
||||
err := survey.AskOne(prompt, &selection)
|
||||
err := forms.Select("Select an event:", core.ReactableEvents, &selection)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -96,9 +92,6 @@ func selectEventType(con *console.SliverClient) (string, error) {
|
||||
|
||||
func userCommands() (string, error) {
|
||||
text := ""
|
||||
prompt := &survey.Multiline{
|
||||
Message: "Enter commands: ",
|
||||
}
|
||||
err := survey.AskOne(prompt, &text)
|
||||
err := forms.Text("Enter commands:", &text)
|
||||
return text, err
|
||||
}
|
||||
@@ -25,9 +25,9 @@ import (
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/core"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -65,13 +65,8 @@ func selectReaction(con *console.SliverClient) (*core.Reaction, error) {
|
||||
options := strings.Split(outputBuf.String(), "\n")
|
||||
options = options[:len(options)-1] // Remove blank line at the end
|
||||
|
||||
// Prompt user with options
|
||||
prompt := &survey.Select{
|
||||
Message: "Select a reaction:",
|
||||
Options: options,
|
||||
}
|
||||
selection := ""
|
||||
err := survey.AskOne(prompt, &selection)
|
||||
err := forms.Select("Select a reaction:", options, &selection)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ import (
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
)
|
||||
@@ -85,12 +85,8 @@ func SelectSession(onlyAlive bool, con *console.SliverClient) (*clientpb.Session
|
||||
|
||||
options := strings.Split(outputBuf.String(), "\n")
|
||||
options = options[:len(options)-1] // Remove the last empty option
|
||||
prompt := &survey.Select{
|
||||
Message: "Select a session:",
|
||||
Options: options,
|
||||
}
|
||||
selected := ""
|
||||
survey.AskOne(prompt, &selected)
|
||||
_ = forms.Select("Select a session:", options, &selected)
|
||||
if selected == "" {
|
||||
return nil, ErrNoSelection
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
|
||||
},
|
||||
GroupID: consts.GenericHelpGroup,
|
||||
}
|
||||
settingsCmd.Flags().Bool("show", false, "Show current settings without prompting")
|
||||
settingsCmd.AddCommand(&cobra.Command{
|
||||
Use: consts.SaveStr,
|
||||
Short: "Save the current settings to disk",
|
||||
|
||||
@@ -19,9 +19,9 @@ package settings
|
||||
*/
|
||||
|
||||
import (
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -45,8 +45,7 @@ func IsUserAnAdult(con *console.SliverClient) bool {
|
||||
return true
|
||||
}
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "This action is bad OPSEC, are you an adult?"}
|
||||
survey.AskOne(prompt, &confirm, nil)
|
||||
_ = forms.Confirm("This action is bad OPSEC, are you an adult?", &confirm)
|
||||
return confirm
|
||||
}
|
||||
|
||||
|
||||
@@ -19,11 +19,13 @@ package settings
|
||||
*/
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -39,6 +41,25 @@ func SettingsCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
}
|
||||
}
|
||||
|
||||
if shouldRunSettingsForm(cmd, con, args) {
|
||||
tableStyleOptions := make([]string, 0, len(tableStyles))
|
||||
for option := range tableStyles {
|
||||
tableStyleOptions = append(tableStyleOptions, option)
|
||||
}
|
||||
result, err := forms.SettingsForm(con.Settings, tableStyleOptions)
|
||||
if err != nil {
|
||||
if errors.Is(err, forms.ErrUserAborted) {
|
||||
return
|
||||
}
|
||||
con.PrintErrorf("Settings form failed: %s\n", err)
|
||||
return
|
||||
}
|
||||
if err := applySettingsForm(con.Settings, result); err != nil {
|
||||
con.PrintErrorf("Failed to apply settings form values: %s\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tw := table.NewWriter()
|
||||
tw.SetStyle(GetTableStyle(con))
|
||||
tw.AppendHeader(table.Row{"Name", "Value", "Description"})
|
||||
@@ -48,6 +69,7 @@ func SettingsCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
tw.AppendRow(table.Row{"Small Term Width", con.Settings.SmallTermWidth, "Omit some table columns when terminal width is less than this value"})
|
||||
tw.AppendRow(table.Row{"Always Overflow", con.Settings.AlwaysOverflow, "Disable table pagination"})
|
||||
tw.AppendRow(table.Row{"Vim Mode", con.Settings.VimMode, "Navigation mode, vim style"})
|
||||
tw.AppendRow(table.Row{"User Connect", con.Settings.UserConnect, "Show operator connect/disconnect events"})
|
||||
tw.AppendRow(table.Row{"Console Logs", con.Settings.ConsoleLogs, "Log console output to disk"})
|
||||
con.Printf("%s\n", tw.Render())
|
||||
}
|
||||
@@ -91,8 +113,7 @@ func SettingsSmallTerm(cmd *cobra.Command, con *console.SliverClient, args []str
|
||||
}
|
||||
}
|
||||
result := ""
|
||||
prompt := &survey.Input{Message: "Set small width:"}
|
||||
err = survey.AskOne(prompt, &result)
|
||||
err = forms.Input("Set small width:", &result)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
@@ -126,11 +147,7 @@ func SettingsTablesCmd(cmd *cobra.Command, con *console.SliverClient, args []str
|
||||
options = append(options, option)
|
||||
}
|
||||
style := ""
|
||||
prompt := &survey.Select{
|
||||
Message: "Choose a style:",
|
||||
Options: options,
|
||||
}
|
||||
err = survey.AskOne(prompt, &style)
|
||||
err = forms.Select("Choose a style:", options, &style)
|
||||
if err != nil {
|
||||
con.PrintErrorf("No selection\n")
|
||||
return
|
||||
@@ -173,3 +190,45 @@ func SettingsUserConnect(cmd *cobra.Command, con *console.SliverClient, args []s
|
||||
con.Settings.UserConnect = !con.Settings.UserConnect
|
||||
con.PrintInfof("User connect events = %v\n", con.Settings.UserConnect)
|
||||
}
|
||||
|
||||
func shouldRunSettingsForm(cmd *cobra.Command, con *console.SliverClient, args []string) bool {
|
||||
if con == nil || con.IsCLI {
|
||||
return false
|
||||
}
|
||||
if len(args) != 0 {
|
||||
return false
|
||||
}
|
||||
if show, err := cmd.Flags().GetBool("show"); err == nil && show {
|
||||
return false
|
||||
}
|
||||
return cmd.Flags().NFlag() == 0
|
||||
}
|
||||
|
||||
func applySettingsForm(settings *assets.ClientSettings, result *forms.SettingsFormResult) error {
|
||||
if settings == nil {
|
||||
return errors.New("settings are required")
|
||||
}
|
||||
if result == nil {
|
||||
return errors.New("settings form result is required")
|
||||
}
|
||||
if _, ok := tableStyles[result.TableStyle]; !ok {
|
||||
return errors.New("invalid table style")
|
||||
}
|
||||
width, err := strconv.Atoi(strings.TrimSpace(result.SmallTermWidth))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if width < 1 {
|
||||
return errors.New("small terminal width must be 1 or greater")
|
||||
}
|
||||
|
||||
settings.TableStyle = result.TableStyle
|
||||
settings.AutoAdult = result.AutoAdult
|
||||
settings.BeaconAutoResults = result.BeaconAutoResults
|
||||
settings.SmallTermWidth = width
|
||||
settings.AlwaysOverflow = result.AlwaysOverflow
|
||||
settings.VimMode = result.VimMode
|
||||
settings.UserConnect = result.UserConnect
|
||||
settings.ConsoleLogs = result.ConsoleLogs
|
||||
return nil
|
||||
}
|
||||
@@ -22,9 +22,9 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/jedib0t/go-pretty/v6/text"
|
||||
"golang.org/x/term"
|
||||
@@ -212,10 +212,7 @@ func PaginateTable(tw table.Writer, skipPages int, overflow bool, interactive bo
|
||||
if 1 < len(pages) {
|
||||
if pageNumber+1 < len(pages) {
|
||||
nextPage := false
|
||||
prompt := &survey.Confirm{
|
||||
Message: fmt.Sprintf("[%d/%d] Continue?", pageNumber+1, len(pages)),
|
||||
}
|
||||
survey.AskOne(prompt, &nextPage)
|
||||
_ = forms.Confirm(fmt.Sprintf("[%d/%d] Continue?", pageNumber+1, len(pages)), &nextPage)
|
||||
if !nextPage {
|
||||
break
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ import (
|
||||
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/core"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/AlecAivazis/survey.v1"
|
||||
)
|
||||
|
||||
// SocksStartCmd - Add a new tunneled port forward.
|
||||
@@ -56,8 +56,7 @@ func SocksStartCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
|
||||
con.PrintWarnf("SOCKS proxy authentication credentials are tunneled to the implant\n")
|
||||
con.PrintWarnf("These credentials are recoverable from the implant's memory!\n\n")
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "Do you understand the implication?"}
|
||||
survey.AskOne(prompt, &confirm, nil)
|
||||
_ = forms.Confirm("Do you understand the implication?", &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -29,10 +29,10 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/command/help"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
)
|
||||
@@ -214,12 +214,8 @@ func SelectMultipleBeaconsAndSessions(con *console.SliverClient) ([]*clientpb.Se
|
||||
|
||||
options := strings.Split(outputBuf.String(), "\n")
|
||||
options = options[:len(options)-1] // Remove the last empty option
|
||||
prompt := &survey.MultiSelect{
|
||||
Message: "Select sessions and beacons:",
|
||||
Options: options,
|
||||
}
|
||||
selected := []string{}
|
||||
survey.AskOne(prompt, &selected)
|
||||
_ = forms.MultiSelect("Select sessions and beacons:", options, &selected)
|
||||
|
||||
if len(selected) == 0 {
|
||||
return nil, nil, fmt.Errorf("no sessions or beacons selected 🤔")
|
||||
|
||||
@@ -24,7 +24,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/command/environment"
|
||||
"github.com/bishopfox/sliver/client/command/exec"
|
||||
"github.com/bishopfox/sliver/client/command/extensions"
|
||||
@@ -36,6 +35,7 @@ import (
|
||||
"github.com/bishopfox/sliver/client/command/settings"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/sliverpb"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
@@ -750,11 +750,7 @@ func taskResponseDownload(download *sliverpb.Download, con *console.SliverClient
|
||||
saveTo = "Save to File ..."
|
||||
)
|
||||
action := saveTo
|
||||
prompt := &survey.Select{
|
||||
Message: "Choose an option:",
|
||||
Options: []string{dump, saveTo},
|
||||
}
|
||||
err := survey.AskOne(prompt, &action, survey.WithValidator(survey.Required))
|
||||
err := forms.SelectRequired("Choose an option:", []string{dump, saveTo}, &action)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
@@ -769,16 +765,14 @@ func taskResponseDownload(download *sliverpb.Download, con *console.SliverClient
|
||||
|
||||
func promptSaveToFile(data []byte, con *console.SliverClient) {
|
||||
saveTo := ""
|
||||
saveToPrompt := &survey.Input{Message: "Save to: "}
|
||||
err := survey.AskOne(saveToPrompt, &saveTo)
|
||||
err := forms.Input("Save to:", &saveTo)
|
||||
if err != nil {
|
||||
con.PrintErrorf("%s\n", err)
|
||||
return
|
||||
}
|
||||
if _, err := os.Stat(saveTo); !os.IsNotExist(err) {
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "Overwrite existing file?"}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
_ = forms.Confirm("Overwrite existing file?", &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/rsteube/carapace"
|
||||
)
|
||||
@@ -32,11 +32,7 @@ func SelectBeaconTask(tasks []*clientpb.BeaconTask) (*clientpb.BeaconTask, error
|
||||
}
|
||||
|
||||
selected := ""
|
||||
prompt := &survey.Select{
|
||||
Message: "Select a beacon task:",
|
||||
Options: options,
|
||||
}
|
||||
err := survey.AskOne(prompt, &selected)
|
||||
err := forms.Select("Select a beacon task:", options, &selected)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -28,16 +28,15 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
consts "github.com/bishopfox/sliver/client/constants"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/client/version"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/util"
|
||||
@@ -57,14 +56,12 @@ func UpdateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
con.Println()
|
||||
con.Println(console.Warn + "You're trying to update over an insecure connection, this is a really bad idea!")
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "Recklessly update?"}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
forms.Confirm("Recklessly update?", &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
confirm = false
|
||||
prompt = &survey.Confirm{Message: "Seriously?"}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
forms.Confirm("Seriously?", &confirm)
|
||||
if !confirm {
|
||||
return
|
||||
}
|
||||
@@ -115,9 +112,9 @@ func UpdateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
|
||||
con.PrintInfof("No new releases.\n")
|
||||
}
|
||||
now := time.Now()
|
||||
lastCheck := []byte(fmt.Sprintf("%d", now.Unix()))
|
||||
lastCheck := fmt.Appendf(nil, "%d", now.Unix())
|
||||
appDir := assets.GetRootAppDir()
|
||||
lastUpdateCheckPath := path.Join(appDir, consts.LastUpdateCheckFileName)
|
||||
lastUpdateCheckPath := filepath.Join(appDir, consts.LastUpdateCheckFileName)
|
||||
err = os.WriteFile(lastUpdateCheckPath, lastCheck, 0o600)
|
||||
if err != nil {
|
||||
con.Printf("Failed to save update check time %s", err)
|
||||
@@ -231,10 +228,7 @@ func updateAvailable(con *console.SliverClient, client *http.Client, release *ve
|
||||
con.Println()
|
||||
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{
|
||||
Message: "Download update?",
|
||||
}
|
||||
survey.AskOne(prompt, &confirm)
|
||||
forms.Confirm("Download update?", &confirm)
|
||||
if confirm {
|
||||
con.Printf("Please wait ...")
|
||||
err := downloadAsset(client, serverAsset, saveTo)
|
||||
|
||||
@@ -27,12 +27,12 @@ import (
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/rsteube/carapace"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/bishopfox/sliver/client/command/beacons"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
)
|
||||
@@ -74,8 +74,7 @@ func SessionOrBeaconByID(id string, con *console.SliverClient) (*clientpb.Sessio
|
||||
sessions, err := con.Rpc.GetSessions(context.Background(), &commonpb.Empty{})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if err == nil {
|
||||
} else {
|
||||
for _, session := range sessions.Sessions {
|
||||
if strings.HasPrefix(session.ID, id) {
|
||||
return session, nil, nil
|
||||
@@ -162,12 +161,8 @@ func SelectSessionOrBeacon(con *console.SliverClient) (*clientpb.Session, *clien
|
||||
|
||||
options := strings.Split(outputBuf.String(), "\n")
|
||||
options = options[:len(options)-1] // Remove the last empty option
|
||||
prompt := &survey.Select{
|
||||
Message: "Select a session or beacon:",
|
||||
Options: options,
|
||||
}
|
||||
selected := ""
|
||||
survey.AskOne(prompt, &selected)
|
||||
_ = forms.Select("Select a session or beacon:", options, &selected)
|
||||
if selected == "" {
|
||||
return nil, nil, ErrNoSelection
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/bishopfox/sliver/client/console"
|
||||
"github.com/bishopfox/sliver/client/forms"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -130,8 +130,7 @@ func webAddFile(web *clientpb.WebsiteAddContent, webpath string, contentType str
|
||||
|
||||
func confirmAddDirectory() bool {
|
||||
confirm := false
|
||||
prompt := &survey.Confirm{Message: "Recursively add entire directory?"}
|
||||
survey.AskOne(prompt, &confirm, nil)
|
||||
_ = forms.Confirm("Recursively add entire directory?", &confirm)
|
||||
return confirm
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ func GenerateForm() (*GenerateFormResult, error) {
|
||||
}
|
||||
}
|
||||
}, &result.OS).
|
||||
Height(3).
|
||||
Value(&result.Arch),
|
||||
huh.NewSelect[string]().
|
||||
Title("Output format").
|
||||
@@ -78,6 +79,7 @@ func GenerateForm() (*GenerateFormResult, error) {
|
||||
}
|
||||
return options
|
||||
}, &result.OS).
|
||||
Height(3).
|
||||
Value(&result.Format),
|
||||
),
|
||||
huh.NewGroup(
|
||||
@@ -107,6 +109,7 @@ func GenerateForm() (*GenerateFormResult, error) {
|
||||
}
|
||||
return options
|
||||
}, &result.OS).
|
||||
Height(6).
|
||||
Value(&result.C2Type),
|
||||
huh.NewInput().
|
||||
Title("C2 connection string(s)").
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
package forms
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/charmbracelet/huh"
|
||||
)
|
||||
|
||||
// GenerateBeaconFormResult captures the inputs needed to drive the generate beacon command.
|
||||
type GenerateBeaconFormResult struct {
|
||||
OS string
|
||||
Arch string
|
||||
Format string
|
||||
Name string
|
||||
C2Type string
|
||||
C2Value string
|
||||
Save string
|
||||
Days string
|
||||
Hours string
|
||||
Minutes string
|
||||
Seconds string
|
||||
Jitter string
|
||||
}
|
||||
|
||||
// GenerateBeaconForm prompts for generate beacon flags and returns the collected values.
|
||||
func GenerateBeaconForm() (*GenerateBeaconFormResult, error) {
|
||||
result := &GenerateBeaconFormResult{
|
||||
OS: "windows",
|
||||
Arch: "amd64",
|
||||
Format: "exe",
|
||||
C2Type: "mtls",
|
||||
}
|
||||
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewSelect[string]().
|
||||
Title("Target operating system").
|
||||
Options(
|
||||
huh.NewOption("Windows", "windows"),
|
||||
huh.NewOption("Linux", "linux"),
|
||||
huh.NewOption("macOS", "darwin"),
|
||||
).
|
||||
Value(&result.OS),
|
||||
huh.NewSelect[string]().
|
||||
Title("CPU architecture").
|
||||
OptionsFunc(func() []huh.Option[string] {
|
||||
switch result.OS {
|
||||
case "darwin":
|
||||
return []huh.Option[string]{
|
||||
huh.NewOption("amd64", "amd64"),
|
||||
huh.NewOption("arm64", "arm64"),
|
||||
}
|
||||
case "linux":
|
||||
return []huh.Option[string]{
|
||||
huh.NewOption("amd64", "amd64"),
|
||||
huh.NewOption("386", "386"),
|
||||
}
|
||||
default:
|
||||
return []huh.Option[string]{
|
||||
huh.NewOption("amd64", "amd64"),
|
||||
huh.NewOption("386", "386"),
|
||||
}
|
||||
}
|
||||
}, &result.OS).
|
||||
Height(3).
|
||||
Value(&result.Arch),
|
||||
huh.NewSelect[string]().
|
||||
Title("Output format").
|
||||
OptionsFunc(func() []huh.Option[string] {
|
||||
options := []huh.Option[string]{
|
||||
huh.NewOption("Executable", "exe"),
|
||||
huh.NewOption("Shared library", "shared"),
|
||||
}
|
||||
if result.OS == "windows" {
|
||||
options = append(options,
|
||||
huh.NewOption("Service", "service"),
|
||||
huh.NewOption("Shellcode", "shellcode"),
|
||||
)
|
||||
}
|
||||
return options
|
||||
}, &result.OS).
|
||||
Height(3).
|
||||
Value(&result.Format),
|
||||
),
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Title("Implant name (optional)").
|
||||
Value(&result.Name).
|
||||
Validate(func(value string) error {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return nil
|
||||
}
|
||||
return util.AllowedName(strings.ToLower(value))
|
||||
}),
|
||||
huh.NewSelect[string]().
|
||||
Title("C2 transport").
|
||||
OptionsFunc(func() []huh.Option[string] {
|
||||
options := []huh.Option[string]{
|
||||
huh.NewOption("mTLS", "mtls"),
|
||||
huh.NewOption("WireGuard", "wg"),
|
||||
huh.NewOption("HTTP(S)", "http"),
|
||||
huh.NewOption("DNS", "dns"),
|
||||
}
|
||||
if result.OS == "windows" {
|
||||
options = append(options,
|
||||
huh.NewOption("Named pipe", "named-pipe"),
|
||||
huh.NewOption("TCP pivot", "tcp-pivot"),
|
||||
)
|
||||
}
|
||||
return options
|
||||
}, &result.OS).
|
||||
Height(6).
|
||||
Value(&result.C2Type),
|
||||
huh.NewInput().
|
||||
Title("C2 connection string(s)").
|
||||
Description("Comma separated; example: 1.2.3.4:8888 or https://c2.example.com").
|
||||
Value(&result.C2Value).
|
||||
Validate(func(value string) error {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return errors.New("connection string required")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
huh.NewInput().
|
||||
Title("Save path (optional)").
|
||||
Description("File or directory; defaults to the current directory").
|
||||
Value(&result.Save),
|
||||
),
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Title("Beacon interval days (optional)").
|
||||
Description("Leave blank for 0").
|
||||
Value(&result.Days).
|
||||
Validate(validateOptionalNonNegativeInt),
|
||||
huh.NewInput().
|
||||
Title("Beacon interval hours (optional)").
|
||||
Description("Leave blank for 0").
|
||||
Value(&result.Hours).
|
||||
Validate(validateOptionalNonNegativeInt),
|
||||
huh.NewInput().
|
||||
Title("Beacon interval minutes (optional)").
|
||||
Description("Leave blank for 0").
|
||||
Value(&result.Minutes).
|
||||
Validate(validateOptionalNonNegativeInt),
|
||||
huh.NewInput().
|
||||
Title("Beacon interval seconds (optional)").
|
||||
Description("Leave blank for default 60 seconds unless other units are set").
|
||||
Value(&result.Seconds).
|
||||
Validate(validateOptionalNonNegativeInt),
|
||||
huh.NewInput().
|
||||
Title("Beacon jitter seconds (optional)").
|
||||
Description("Leave blank for default 30 seconds").
|
||||
Value(&result.Jitter).
|
||||
Validate(validateOptionalNonNegativeInt),
|
||||
),
|
||||
)
|
||||
|
||||
if err := form.Run(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func validateOptionalNonNegativeInt(value string) error {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" {
|
||||
return nil
|
||||
}
|
||||
parsed, err := strconv.ParseInt(trimmed, 10, 64)
|
||||
if err != nil {
|
||||
return errors.New("must be a whole number")
|
||||
}
|
||||
if parsed < 0 {
|
||||
return errors.New("must be 0 or greater")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package forms
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/charmbracelet/huh"
|
||||
)
|
||||
|
||||
// GenerateProfilesNewFormResult captures the inputs needed to drive the profiles new command.
|
||||
type GenerateProfilesNewFormResult struct {
|
||||
ProfileName string
|
||||
OS string
|
||||
Arch string
|
||||
Format string
|
||||
Name string
|
||||
C2Type string
|
||||
C2Value string
|
||||
}
|
||||
|
||||
// GenerateProfilesNewForm prompts for profiles new flags and returns the collected values.
|
||||
func GenerateProfilesNewForm() (*GenerateProfilesNewFormResult, error) {
|
||||
result := &GenerateProfilesNewFormResult{
|
||||
OS: "windows",
|
||||
Arch: "amd64",
|
||||
Format: "exe",
|
||||
C2Type: "mtls",
|
||||
}
|
||||
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Title("Profile name").
|
||||
Value(&result.ProfileName).
|
||||
Validate(func(value string) error {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" {
|
||||
return errors.New("profile name required")
|
||||
}
|
||||
return util.AllowedName(trimmed)
|
||||
}),
|
||||
huh.NewSelect[string]().
|
||||
Title("Target operating system").
|
||||
Options(
|
||||
huh.NewOption("Windows", "windows"),
|
||||
huh.NewOption("Linux", "linux"),
|
||||
huh.NewOption("macOS", "darwin"),
|
||||
).
|
||||
Value(&result.OS),
|
||||
huh.NewSelect[string]().
|
||||
Title("CPU architecture").
|
||||
OptionsFunc(func() []huh.Option[string] {
|
||||
switch result.OS {
|
||||
case "darwin":
|
||||
return []huh.Option[string]{
|
||||
huh.NewOption("amd64", "amd64"),
|
||||
huh.NewOption("arm64", "arm64"),
|
||||
}
|
||||
case "linux":
|
||||
return []huh.Option[string]{
|
||||
huh.NewOption("amd64", "amd64"),
|
||||
huh.NewOption("386", "386"),
|
||||
}
|
||||
default:
|
||||
return []huh.Option[string]{
|
||||
huh.NewOption("amd64", "amd64"),
|
||||
huh.NewOption("386", "386"),
|
||||
}
|
||||
}
|
||||
}, &result.OS).
|
||||
Height(3).
|
||||
Value(&result.Arch),
|
||||
huh.NewSelect[string]().
|
||||
Title("Output format").
|
||||
OptionsFunc(func() []huh.Option[string] {
|
||||
options := []huh.Option[string]{
|
||||
huh.NewOption("Executable", "exe"),
|
||||
huh.NewOption("Shared library", "shared"),
|
||||
}
|
||||
if result.OS == "windows" {
|
||||
options = append(options,
|
||||
huh.NewOption("Service", "service"),
|
||||
huh.NewOption("Shellcode", "shellcode"),
|
||||
)
|
||||
}
|
||||
return options
|
||||
}, &result.OS).
|
||||
Height(3).
|
||||
Value(&result.Format),
|
||||
),
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Title("Implant name (optional)").
|
||||
Value(&result.Name).
|
||||
Validate(func(value string) error {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return nil
|
||||
}
|
||||
return util.AllowedName(strings.ToLower(value))
|
||||
}),
|
||||
huh.NewSelect[string]().
|
||||
Title("C2 transport").
|
||||
OptionsFunc(func() []huh.Option[string] {
|
||||
options := []huh.Option[string]{
|
||||
huh.NewOption("mTLS", "mtls"),
|
||||
huh.NewOption("WireGuard", "wg"),
|
||||
huh.NewOption("HTTP(S)", "http"),
|
||||
huh.NewOption("DNS", "dns"),
|
||||
}
|
||||
if result.OS == "windows" {
|
||||
options = append(options,
|
||||
huh.NewOption("Named pipe", "named-pipe"),
|
||||
huh.NewOption("TCP pivot", "tcp-pivot"),
|
||||
)
|
||||
}
|
||||
return options
|
||||
}, &result.OS).
|
||||
Height(6).
|
||||
Value(&result.C2Type),
|
||||
huh.NewInput().
|
||||
Title("C2 connection string(s)").
|
||||
Description("Comma separated; example: 1.2.3.4:8888 or https://c2.example.com").
|
||||
Value(&result.C2Value).
|
||||
Validate(func(value string) error {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return errors.New("connection string required")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
if err := form.Run(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package forms
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/util"
|
||||
"github.com/charmbracelet/huh"
|
||||
)
|
||||
|
||||
// GenerateProfilesNewBeaconFormResult captures the inputs needed to drive the profiles new beacon command.
|
||||
type GenerateProfilesNewBeaconFormResult struct {
|
||||
ProfileName string
|
||||
OS string
|
||||
Arch string
|
||||
Format string
|
||||
Name string
|
||||
C2Type string
|
||||
C2Value string
|
||||
Days string
|
||||
Hours string
|
||||
Minutes string
|
||||
Seconds string
|
||||
Jitter string
|
||||
}
|
||||
|
||||
// GenerateProfilesNewBeaconForm prompts for profiles new beacon flags and returns the collected values.
|
||||
func GenerateProfilesNewBeaconForm() (*GenerateProfilesNewBeaconFormResult, error) {
|
||||
result := &GenerateProfilesNewBeaconFormResult{
|
||||
OS: "windows",
|
||||
Arch: "amd64",
|
||||
Format: "exe",
|
||||
C2Type: "mtls",
|
||||
}
|
||||
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Title("Profile name").
|
||||
Value(&result.ProfileName).
|
||||
Validate(func(value string) error {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" {
|
||||
return errors.New("profile name required")
|
||||
}
|
||||
return util.AllowedName(trimmed)
|
||||
}),
|
||||
huh.NewSelect[string]().
|
||||
Title("Target operating system").
|
||||
Options(
|
||||
huh.NewOption("Windows", "windows"),
|
||||
huh.NewOption("Linux", "linux"),
|
||||
huh.NewOption("macOS", "darwin"),
|
||||
).
|
||||
Value(&result.OS),
|
||||
huh.NewSelect[string]().
|
||||
Title("CPU architecture").
|
||||
OptionsFunc(func() []huh.Option[string] {
|
||||
switch result.OS {
|
||||
case "darwin":
|
||||
return []huh.Option[string]{
|
||||
huh.NewOption("amd64", "amd64"),
|
||||
huh.NewOption("arm64", "arm64"),
|
||||
}
|
||||
case "linux":
|
||||
return []huh.Option[string]{
|
||||
huh.NewOption("amd64", "amd64"),
|
||||
huh.NewOption("386", "386"),
|
||||
}
|
||||
default:
|
||||
return []huh.Option[string]{
|
||||
huh.NewOption("amd64", "amd64"),
|
||||
huh.NewOption("386", "386"),
|
||||
}
|
||||
}
|
||||
}, &result.OS).
|
||||
Height(3).
|
||||
Value(&result.Arch),
|
||||
huh.NewSelect[string]().
|
||||
Title("Output format").
|
||||
OptionsFunc(func() []huh.Option[string] {
|
||||
options := []huh.Option[string]{
|
||||
huh.NewOption("Executable", "exe"),
|
||||
huh.NewOption("Shared library", "shared"),
|
||||
}
|
||||
if result.OS == "windows" {
|
||||
options = append(options,
|
||||
huh.NewOption("Service", "service"),
|
||||
huh.NewOption("Shellcode", "shellcode"),
|
||||
)
|
||||
}
|
||||
return options
|
||||
}, &result.OS).
|
||||
Height(3).
|
||||
Value(&result.Format),
|
||||
),
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Title("Implant name (optional)").
|
||||
Value(&result.Name).
|
||||
Validate(func(value string) error {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return nil
|
||||
}
|
||||
return util.AllowedName(strings.ToLower(value))
|
||||
}),
|
||||
huh.NewSelect[string]().
|
||||
Title("C2 transport").
|
||||
OptionsFunc(func() []huh.Option[string] {
|
||||
options := []huh.Option[string]{
|
||||
huh.NewOption("mTLS", "mtls"),
|
||||
huh.NewOption("WireGuard", "wg"),
|
||||
huh.NewOption("HTTP(S)", "http"),
|
||||
huh.NewOption("DNS", "dns"),
|
||||
}
|
||||
if result.OS == "windows" {
|
||||
options = append(options,
|
||||
huh.NewOption("Named pipe", "named-pipe"),
|
||||
huh.NewOption("TCP pivot", "tcp-pivot"),
|
||||
)
|
||||
}
|
||||
return options
|
||||
}, &result.OS).
|
||||
Height(6).
|
||||
Value(&result.C2Type),
|
||||
huh.NewInput().
|
||||
Title("C2 connection string(s)").
|
||||
Description("Comma separated; example: 1.2.3.4:8888 or https://c2.example.com").
|
||||
Value(&result.C2Value).
|
||||
Validate(func(value string) error {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return errors.New("connection string required")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
),
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Title("Beacon interval days (optional)").
|
||||
Description("Leave blank for 0").
|
||||
Value(&result.Days).
|
||||
Validate(validateOptionalNonNegativeInt),
|
||||
huh.NewInput().
|
||||
Title("Beacon interval hours (optional)").
|
||||
Description("Leave blank for 0").
|
||||
Value(&result.Hours).
|
||||
Validate(validateOptionalNonNegativeInt),
|
||||
huh.NewInput().
|
||||
Title("Beacon interval minutes (optional)").
|
||||
Description("Leave blank for 0").
|
||||
Value(&result.Minutes).
|
||||
Validate(validateOptionalNonNegativeInt),
|
||||
huh.NewInput().
|
||||
Title("Beacon interval seconds (optional)").
|
||||
Description("Leave blank for default 60 seconds unless other units are set").
|
||||
Value(&result.Seconds).
|
||||
Validate(validateOptionalNonNegativeInt),
|
||||
huh.NewInput().
|
||||
Title("Beacon jitter seconds (optional)").
|
||||
Description("Leave blank for default 30 seconds").
|
||||
Value(&result.Jitter).
|
||||
Validate(validateOptionalNonNegativeInt),
|
||||
),
|
||||
)
|
||||
|
||||
if err := form.Run(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package forms
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
)
|
||||
|
||||
const defaultSelectHeight = 10
|
||||
|
||||
// Confirm prompts for a yes/no answer.
|
||||
func Confirm(title string, value *bool) error {
|
||||
if value == nil {
|
||||
return errors.New("confirm value is required")
|
||||
}
|
||||
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewConfirm().
|
||||
Title(title).
|
||||
Value(value),
|
||||
),
|
||||
)
|
||||
|
||||
return form.Run()
|
||||
}
|
||||
|
||||
// Input prompts for a single-line string.
|
||||
func Input(title string, value *string) error {
|
||||
if value == nil {
|
||||
return errors.New("input value is required")
|
||||
}
|
||||
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().
|
||||
Title(title).
|
||||
Value(value),
|
||||
),
|
||||
)
|
||||
|
||||
return form.Run()
|
||||
}
|
||||
|
||||
// Text prompts for multi-line input.
|
||||
func Text(title string, value *string) error {
|
||||
if value == nil {
|
||||
return errors.New("text value is required")
|
||||
}
|
||||
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewText().
|
||||
Title(title).
|
||||
ExternalEditor(false).
|
||||
Value(value),
|
||||
),
|
||||
)
|
||||
|
||||
return form.Run()
|
||||
}
|
||||
|
||||
// Select prompts for a single selection from options.
|
||||
func Select(title string, options []string, value *string) error {
|
||||
return selectPrompt(title, options, value, false)
|
||||
}
|
||||
|
||||
// SelectRequired prompts for a single selection and requires a non-empty value.
|
||||
func SelectRequired(title string, options []string, value *string) error {
|
||||
return selectPrompt(title, options, value, true)
|
||||
}
|
||||
|
||||
// MultiSelect prompts for selecting multiple values.
|
||||
func MultiSelect(title string, options []string, value *[]string) error {
|
||||
if value == nil {
|
||||
return errors.New("multi-select value is required")
|
||||
}
|
||||
if len(options) == 0 {
|
||||
return errors.New("multi-select options are required")
|
||||
}
|
||||
|
||||
field := huh.NewMultiSelect[string]().
|
||||
Title(title).
|
||||
Options(makeStringOptions(options)...).
|
||||
Height(listHeight(len(options))).
|
||||
Value(value)
|
||||
|
||||
form := huh.NewForm(huh.NewGroup(field))
|
||||
return form.Run()
|
||||
}
|
||||
|
||||
func selectPrompt(title string, options []string, value *string, required bool) error {
|
||||
if value == nil {
|
||||
return errors.New("select value is required")
|
||||
}
|
||||
if len(options) == 0 {
|
||||
return errors.New("select options are required")
|
||||
}
|
||||
|
||||
ensureSelectedValue(options, value)
|
||||
|
||||
field := huh.NewSelect[string]().
|
||||
Title(title).
|
||||
Options(makeStringOptions(options)...).
|
||||
Height(listHeight(len(options))).
|
||||
Value(value)
|
||||
|
||||
if required {
|
||||
field.Validate(func(val string) error {
|
||||
if strings.TrimSpace(val) == "" {
|
||||
return errors.New("selection required")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
form := huh.NewForm(huh.NewGroup(field))
|
||||
return form.Run()
|
||||
}
|
||||
|
||||
func ensureSelectedValue(options []string, value *string) {
|
||||
if value == nil || len(options) == 0 {
|
||||
return
|
||||
}
|
||||
if *value == "" {
|
||||
*value = options[0]
|
||||
return
|
||||
}
|
||||
for _, option := range options {
|
||||
if option == *value {
|
||||
return
|
||||
}
|
||||
}
|
||||
*value = options[0]
|
||||
}
|
||||
|
||||
func makeStringOptions(options []string) []huh.Option[string] {
|
||||
converted := make([]huh.Option[string], 0, len(options))
|
||||
for _, option := range options {
|
||||
converted = append(converted, huh.NewOption(option, option))
|
||||
}
|
||||
return converted
|
||||
}
|
||||
|
||||
func listHeight(count int) int {
|
||||
if count < 1 {
|
||||
return 1
|
||||
}
|
||||
if count > defaultSelectHeight {
|
||||
return defaultSelectHeight
|
||||
}
|
||||
return count
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package forms
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/charmbracelet/huh"
|
||||
)
|
||||
|
||||
// SettingsFormResult captures the inputs needed to update client settings.
|
||||
type SettingsFormResult struct {
|
||||
TableStyle string
|
||||
AutoAdult bool
|
||||
BeaconAutoResults bool
|
||||
SmallTermWidth string
|
||||
AlwaysOverflow bool
|
||||
VimMode bool
|
||||
UserConnect bool
|
||||
ConsoleLogs bool
|
||||
}
|
||||
|
||||
// SettingsForm prompts for client settings and returns the collected values.
|
||||
func SettingsForm(settings *assets.ClientSettings, tableStyleOptions []string) (*SettingsFormResult, error) {
|
||||
if settings == nil {
|
||||
return nil, errors.New("settings are required")
|
||||
}
|
||||
if len(tableStyleOptions) == 0 {
|
||||
return nil, errors.New("no table styles available")
|
||||
}
|
||||
|
||||
result := &SettingsFormResult{
|
||||
TableStyle: settings.TableStyle,
|
||||
AutoAdult: settings.AutoAdult,
|
||||
BeaconAutoResults: settings.BeaconAutoResults,
|
||||
SmallTermWidth: strconv.Itoa(settings.SmallTermWidth),
|
||||
AlwaysOverflow: settings.AlwaysOverflow,
|
||||
VimMode: settings.VimMode,
|
||||
UserConnect: settings.UserConnect,
|
||||
ConsoleLogs: settings.ConsoleLogs,
|
||||
}
|
||||
if result.TableStyle == "" {
|
||||
result.TableStyle = tableStyleOptions[0]
|
||||
}
|
||||
|
||||
styleOptions := make([]huh.Option[string], 0, len(tableStyleOptions))
|
||||
for _, option := range tableStyleOptions {
|
||||
styleOptions = append(styleOptions, huh.NewOption(option, option))
|
||||
}
|
||||
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewSelect[string]().
|
||||
Title("Table style").
|
||||
Options(styleOptions...).
|
||||
Height(len(styleOptions)).
|
||||
Value(&result.TableStyle),
|
||||
huh.NewInput().
|
||||
Title("Small terminal width").
|
||||
Description("Omit some table columns when the terminal is narrower than this value").
|
||||
Value(&result.SmallTermWidth).
|
||||
Validate(validateRequiredPositiveInt),
|
||||
huh.NewConfirm().
|
||||
Title("Always overflow tables").
|
||||
Description("Disable table pagination").
|
||||
Value(&result.AlwaysOverflow),
|
||||
).Title("Display"),
|
||||
huh.NewGroup(
|
||||
huh.NewConfirm().
|
||||
Title("Auto accept OPSEC warnings").
|
||||
Description("Skip confirmation prompts for risky actions").
|
||||
Value(&result.AutoAdult),
|
||||
huh.NewConfirm().
|
||||
Title("Auto display beacon results").
|
||||
Description("Show beacon task results when tasks complete").
|
||||
Value(&result.BeaconAutoResults),
|
||||
huh.NewConfirm().
|
||||
Title("Vim navigation mode").
|
||||
Description("Use vim-style navigation in the console").
|
||||
Value(&result.VimMode),
|
||||
huh.NewConfirm().
|
||||
Title("User connect events").
|
||||
Description("Show operator connect and disconnect events").
|
||||
Value(&result.UserConnect),
|
||||
huh.NewConfirm().
|
||||
Title("Console logs").
|
||||
Description("Log console output to disk").
|
||||
Value(&result.ConsoleLogs),
|
||||
).Title("Behavior"),
|
||||
)
|
||||
|
||||
if err := form.Run(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func validateRequiredPositiveInt(value string) error {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" {
|
||||
return errors.New("value required")
|
||||
}
|
||||
parsed, err := strconv.Atoi(trimmed)
|
||||
if err != nil {
|
||||
return errors.New("must be a whole number")
|
||||
}
|
||||
if parsed < 1 {
|
||||
return errors.New("must be 1 or greater")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -19,15 +19,23 @@ package version
|
||||
*/
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
const (
|
||||
normal = "\033[0m"
|
||||
bold = "\033[1m"
|
||||
|
||||
defaultVersion = "devel"
|
||||
defaultReleasesURL = "https://api.github.com/repos/BishopFox/sliver/releases"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -47,22 +55,111 @@ var (
|
||||
CompiledAt string
|
||||
)
|
||||
|
||||
func init() {
|
||||
if GithubReleasesURL == "" {
|
||||
GithubReleasesURL = defaultReleasesURL
|
||||
}
|
||||
applyBuildInfo()
|
||||
}
|
||||
|
||||
func applyBuildInfo() {
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
if !ok || info == nil {
|
||||
ensureDefaults()
|
||||
return
|
||||
}
|
||||
|
||||
if GoVersion == "" {
|
||||
if info.GoVersion != "" {
|
||||
GoVersion = info.GoVersion
|
||||
} else {
|
||||
GoVersion = runtime.Version()
|
||||
}
|
||||
}
|
||||
|
||||
if Version == "" {
|
||||
Version = normalizeVersion(info.Main.Version)
|
||||
}
|
||||
|
||||
settings := buildSettings(info.Settings)
|
||||
if GitCommit == "" {
|
||||
GitCommit = settings["vcs.revision"]
|
||||
}
|
||||
if GitDirty == "" && settings["vcs.modified"] == "true" {
|
||||
GitDirty = "Dirty"
|
||||
}
|
||||
if CompiledAt == "" {
|
||||
if vcsTime := settings["vcs.time"]; vcsTime != "" {
|
||||
if compiled, err := time.Parse(time.RFC3339, vcsTime); err == nil {
|
||||
CompiledAt = strconv.FormatInt(compiled.Unix(), 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ensureDefaults()
|
||||
}
|
||||
|
||||
func ensureDefaults() {
|
||||
if Version == "" {
|
||||
Version = defaultVersion
|
||||
}
|
||||
if GoVersion == "" {
|
||||
GoVersion = runtime.Version()
|
||||
}
|
||||
}
|
||||
|
||||
func buildSettings(settings []debug.BuildSetting) map[string]string {
|
||||
values := make(map[string]string, len(settings))
|
||||
for _, setting := range settings {
|
||||
values[setting.Key] = setting.Value
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func normalizeVersion(version string) string {
|
||||
if version == "" || version == "(devel)" {
|
||||
return defaultVersion
|
||||
}
|
||||
if canonical := canonicalSemver(version); canonical != "" {
|
||||
return canonical
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
func canonicalSemver(version string) string {
|
||||
version = strings.TrimSpace(version)
|
||||
if version == "" || version == "(devel)" {
|
||||
return ""
|
||||
}
|
||||
if !strings.HasPrefix(version, "v") {
|
||||
version = "v" + version
|
||||
}
|
||||
return semver.Canonical(version)
|
||||
}
|
||||
|
||||
// SemanticVersion - Get the structured sematic version
|
||||
func SemanticVersion() []int {
|
||||
semVer := []int{}
|
||||
version := Version
|
||||
if strings.HasPrefix(version, "v") {
|
||||
version = version[1:]
|
||||
semVer := make([]int, 3)
|
||||
canonical := canonicalSemver(Version)
|
||||
if canonical == "" {
|
||||
return semVer
|
||||
}
|
||||
for _, part := range strings.Split(version, ".") {
|
||||
number, _ := strconv.Atoi(part)
|
||||
semVer = append(semVer, number)
|
||||
base := strings.TrimPrefix(canonical, "v")
|
||||
if idx := strings.Index(base, "-"); idx != -1 {
|
||||
base = base[:idx]
|
||||
}
|
||||
parts := strings.Split(base, ".")
|
||||
for i := 0; i < 3 && i < len(parts); i++ {
|
||||
semVer[i], _ = strconv.Atoi(parts[i])
|
||||
}
|
||||
return semVer
|
||||
}
|
||||
|
||||
// Compiled - Get time this binary was compiled
|
||||
func Compiled() (time.Time, error) {
|
||||
if CompiledAt == "" {
|
||||
return time.Unix(0, 0), errors.New("compiled time not available")
|
||||
}
|
||||
compiled, err := strconv.ParseInt(CompiledAt, 10, 64)
|
||||
if err != nil {
|
||||
return time.Unix(0, 0), err
|
||||
@@ -74,7 +171,7 @@ func Compiled() (time.Time, error) {
|
||||
func FullVersion() string {
|
||||
ver := fmt.Sprintf("%s", Version)
|
||||
compiled, err := Compiled()
|
||||
if err != nil {
|
||||
if err == nil {
|
||||
ver += fmt.Sprintf(" - Compiled %s", compiled.String())
|
||||
}
|
||||
if GitCommit != "" {
|
||||
|
||||
+12
-33
@@ -6,9 +6,9 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -90,42 +90,21 @@ func CheckForUpdates(client *http.Client, prereleases bool) (*Release, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mySemVer := SemanticVersion()
|
||||
current := canonicalSemver(Version)
|
||||
if current == "" {
|
||||
current = "v0.0.0"
|
||||
}
|
||||
for _, release := range *releases {
|
||||
if release.Prerelease && !prereleases {
|
||||
continue
|
||||
}
|
||||
releaseSemVer := parseGitTag(release.TagName)
|
||||
for index, myVer := range mySemVer {
|
||||
if index < len(releaseSemVer) {
|
||||
if releaseSemVer[index] == myVer {
|
||||
continue
|
||||
}
|
||||
if releaseSemVer[index] < myVer {
|
||||
break
|
||||
}
|
||||
if myVer < releaseSemVer[index] {
|
||||
return release, nil
|
||||
}
|
||||
}
|
||||
releaseVersion := canonicalSemver(release.TagName)
|
||||
if releaseVersion == "" {
|
||||
continue
|
||||
}
|
||||
if semver.Compare(releaseVersion, current) > 0 {
|
||||
return release, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// parseGitTag - Get the structured sematic version
|
||||
func parseGitTag(tag string) []int {
|
||||
semVer := []int{}
|
||||
version := tag
|
||||
if strings.Contains(version, "-") {
|
||||
version = strings.Split(version, "-")[0]
|
||||
}
|
||||
if strings.HasPrefix(version, "v") {
|
||||
version = version[1:]
|
||||
}
|
||||
for _, part := range strings.Split(version, ".") {
|
||||
number, _ := strconv.Atoi(part)
|
||||
semVer = append(semVer, number)
|
||||
}
|
||||
return semVer
|
||||
}
|
||||
@@ -6,7 +6,6 @@ replace github.com/rsteube/carapace v0.36.3 => github.com/reeflective/carapace v
|
||||
|
||||
require (
|
||||
filippo.io/age v1.2.1
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
github.com/Binject/binjection v0.0.0-20210701074423-605d46e35deb
|
||||
github.com/Binject/debug v0.0.0-20230508195519-26db73212a7a
|
||||
github.com/Binject/go-donut v0.0.0-20220908180326-fcdcc35d591c
|
||||
@@ -49,17 +48,17 @@ require (
|
||||
github.com/ulikunitz/xz v0.5.15
|
||||
github.com/xlab/treeprint v1.2.0
|
||||
github.com/yiya1989/sshkrb5 v0.0.1
|
||||
golang.org/x/crypto v0.43.0
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9
|
||||
golang.org/x/net v0.46.0
|
||||
golang.org/x/sys v0.37.0
|
||||
golang.org/x/term v0.36.0
|
||||
golang.org/x/text v0.30.0
|
||||
golang.org/x/mod v0.30.0
|
||||
golang.org/x/net v0.47.0
|
||||
golang.org/x/sys v0.39.0
|
||||
golang.org/x/term v0.38.0
|
||||
golang.org/x/text v0.32.0
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
||||
google.golang.org/grpc v1.76.0
|
||||
google.golang.org/protobuf v1.36.10
|
||||
gopkg.in/AlecAivazis/survey.v1 v1.8.8
|
||||
gorm.io/driver/mysql v1.6.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/driver/sqlite v1.6.0
|
||||
@@ -140,7 +139,6 @@ require (
|
||||
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||
github.com/mdlayher/netlink v1.8.0 // indirect
|
||||
github.com/mdlayher/socket v0.5.1 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||
github.com/moloch--/go-keystone v0.0.2 // indirect
|
||||
@@ -168,11 +166,10 @@ require (
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/mod v0.28.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/time v0.13.0 // indirect
|
||||
golang.org/x/tools v0.37.0 // indirect
|
||||
golang.org/x/tools v0.39.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f // indirect
|
||||
|
||||
@@ -11,9 +11,6 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc=
|
||||
filippo.io/mkcert v1.4.4/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA=
|
||||
github.com/AlecAivazis/survey/v2 v2.0.5/go.mod h1:WYBhg6f0y/fNYUuesWQc0PKbJcEliGcYHB9sNT3Bg74=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
|
||||
github.com/Binject/binjection v0.0.0-20210701074423-605d46e35deb h1:c1Bb1n1wr2xgTHkNVxkUHBshff33m+IXgY6lE6oYttg=
|
||||
github.com/Binject/binjection v0.0.0-20210701074423-605d46e35deb/go.mod h1:fSQ9asCJK/lIGXUnoRwJNMXcnzel53AsZMCtlVBIn3k=
|
||||
github.com/Binject/debug v0.0.0-20210225042342-c9b8b45728d2/go.mod h1:QzgxDLY/qdKlvnbnb65eqTedhvQPbaSP2NqIbcuKvsQ=
|
||||
@@ -35,9 +32,6 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/Ne0nd0g/go-clr v1.0.3 h1:xt92wwuqY23ZSC7RuHD3mKu3K22Bk5NNbxI803vojK4=
|
||||
github.com/Ne0nd0g/go-clr v1.0.3/go.mod h1:TKYSQ/5xT25EvBUttAlUrzpR8yHuI0qTRK495I5xG/I=
|
||||
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
|
||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
|
||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
|
||||
github.com/VirusTotal/vt-go v0.0.0-20210528074736-45bbe34cc8ab/go.mod h1:u1+HeRyl/gQs67eDgVEWNE7+x+zCyXhdtNVrRJR5YPE=
|
||||
github.com/VirusTotal/vt-go v1.0.1 h1:rj/qugIY8XNC6ogwOaeJAGCOsb3nmY63+yuDMtHOx0Q=
|
||||
github.com/VirusTotal/vt-go v1.0.1/go.mod h1:u1+HeRyl/gQs67eDgVEWNE7+x+zCyXhdtNVrRJR5YPE=
|
||||
@@ -150,7 +144,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creachadair/taskgroup v0.13.2 h1:3KyqakBuFsm3KkXi/9XIb0QcA8tEzLHLgaoidf0MdVc=
|
||||
github.com/creachadair/taskgroup v0.13.2/go.mod h1:i3V1Zx7H8RjwljUEeUWYT30Lmb9poewSb2XI1yTwD0g=
|
||||
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -254,9 +247,6 @@ github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/C
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
|
||||
github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
|
||||
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
|
||||
github.com/illarion/gonotify/v3 v3.0.2 h1:O7S6vcopHexutmpObkeWsnzMJt/r1hONIEogeVNmJMk=
|
||||
github.com/illarion/gonotify/v3 v3.0.2/go.mod h1:HWGPdPe817GfvY3w7cx6zkbzNZfi3QjcBm/wgVvEL1U=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
@@ -303,7 +293,6 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
@@ -317,11 +306,9 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
@@ -340,9 +327,6 @@ github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ
|
||||
github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE=
|
||||
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
|
||||
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
||||
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
||||
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
||||
@@ -426,7 +410,6 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
@@ -482,7 +465,6 @@ github.com/yiya1989/sshkrb5 v0.0.1 h1:oAKpXLBeZExjYDV9eTL2KypmXU+5oPrJtqDuR5JbWi
|
||||
github.com/yiya1989/sshkrb5 v0.0.1/go.mod h1:r1bHcNa22x8rrg/Kj/cDF5bX6UtZrHiQgRFvj3Mnpl8=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
@@ -504,13 +486,11 @@ go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wus
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A=
|
||||
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
|
||||
@@ -524,9 +504,8 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -535,10 +514,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
@@ -548,44 +525,34 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210217105451-b926d437f341/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
|
||||
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -597,9 +564,8 @@ golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtn
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -630,8 +596,6 @@ google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
||||
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/AlecAivazis/survey.v1 v1.8.8 h1:5UtTowJZTz1j7NxVzDGKTz6Lm9IWm8DDF6b7a2wq9VY=
|
||||
gopkg.in/AlecAivazis/survey.v1 v1.8.8/go.mod h1:CaHjv79TCgAvXMSFJSVgonHXYWxnhzI3eoHtnX5UgUo=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
|
||||
@@ -28,8 +28,8 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
ver "github.com/bishopfox/sliver/client/version"
|
||||
"github.com/bishopfox/sliver/server/log"
|
||||
ver "github.com/bishopfox/sliver/server/version"
|
||||
"golang.org/x/crypto/openpgp/armor"
|
||||
"golang.org/x/crypto/openpgp/packet"
|
||||
)
|
||||
|
||||
@@ -31,13 +31,13 @@ import (
|
||||
|
||||
clientAssets "github.com/bishopfox/sliver/client/assets"
|
||||
"github.com/bishopfox/sliver/client/transport"
|
||||
"github.com/bishopfox/sliver/client/version"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/server/assets"
|
||||
"github.com/bishopfox/sliver/server/builder"
|
||||
"github.com/bishopfox/sliver/server/c2"
|
||||
"github.com/bishopfox/sliver/server/generate"
|
||||
"github.com/bishopfox/sliver/server/log"
|
||||
"github.com/bishopfox/sliver/server/version"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ package cli
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/bishopfox/sliver/client/version"
|
||||
"github.com/bishopfox/sliver/server/version"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/mod/semver"
|
||||
"gorm.io/gorm/callbacks"
|
||||
|
||||
_ "github.com/ncruces/go-sqlite3"
|
||||
@@ -200,6 +201,27 @@ func (dialectopr Dialector) RollbackTo(tx *gorm.DB, name string) error {
|
||||
}
|
||||
|
||||
func compareVersion(version1, version2 string) int {
|
||||
semver1 := canonicalSQLiteVersion(version1)
|
||||
semver2 := canonicalSQLiteVersion(version2)
|
||||
if semver1 != "" && semver2 != "" {
|
||||
return semver.Compare(semver1, semver2)
|
||||
}
|
||||
|
||||
return compareVersionLegacy(version1, version2)
|
||||
}
|
||||
|
||||
func canonicalSQLiteVersion(version string) string {
|
||||
version = strings.TrimSpace(version)
|
||||
if version == "" {
|
||||
return ""
|
||||
}
|
||||
if !strings.HasPrefix(version, "v") {
|
||||
version = "v" + version
|
||||
}
|
||||
return semver.Canonical(version)
|
||||
}
|
||||
|
||||
func compareVersionLegacy(version1, version2 string) int {
|
||||
n, m := len(version1), len(version2)
|
||||
i, j := 0, 0
|
||||
for i < n || j < m {
|
||||
|
||||
+1
-1
@@ -25,7 +25,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bishopfox/sliver/client/version"
|
||||
"github.com/bishopfox/sliver/protobuf/clientpb"
|
||||
"github.com/bishopfox/sliver/protobuf/commonpb"
|
||||
"github.com/bishopfox/sliver/protobuf/rpcpb"
|
||||
@@ -33,6 +32,7 @@ import (
|
||||
"github.com/bishopfox/sliver/server/core"
|
||||
"github.com/bishopfox/sliver/server/db"
|
||||
"github.com/bishopfox/sliver/server/log"
|
||||
"github.com/bishopfox/sliver/server/version"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/peer"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
package version
|
||||
|
||||
/*
|
||||
Sliver Implant Framework
|
||||
Copyright (C) 2019 Bishop Fox
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
const defaultVersion = "devel"
|
||||
|
||||
var (
|
||||
// Version - The semantic version in string form
|
||||
Version string
|
||||
|
||||
// GoVersion - Go compiler version
|
||||
GoVersion string
|
||||
|
||||
// GitCommit - The commit id at compile time
|
||||
GitCommit string
|
||||
|
||||
// GitDirty - Was the commit dirty at compile time
|
||||
GitDirty string
|
||||
|
||||
// CompiledAt - When was this binary compiled
|
||||
CompiledAt string
|
||||
)
|
||||
|
||||
func init() {
|
||||
applyBuildInfo()
|
||||
}
|
||||
|
||||
func applyBuildInfo() {
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
if !ok || info == nil {
|
||||
ensureDefaults()
|
||||
return
|
||||
}
|
||||
|
||||
if GoVersion == "" {
|
||||
if info.GoVersion != "" {
|
||||
GoVersion = info.GoVersion
|
||||
} else {
|
||||
GoVersion = runtime.Version()
|
||||
}
|
||||
}
|
||||
|
||||
if Version == "" {
|
||||
Version = normalizeVersion(info.Main.Version)
|
||||
}
|
||||
|
||||
settings := buildSettings(info.Settings)
|
||||
if GitCommit == "" {
|
||||
GitCommit = settings["vcs.revision"]
|
||||
}
|
||||
if GitDirty == "" && settings["vcs.modified"] == "true" {
|
||||
GitDirty = "Dirty"
|
||||
}
|
||||
if CompiledAt == "" {
|
||||
if vcsTime := settings["vcs.time"]; vcsTime != "" {
|
||||
if compiled, err := time.Parse(time.RFC3339, vcsTime); err == nil {
|
||||
CompiledAt = strconv.FormatInt(compiled.Unix(), 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ensureDefaults()
|
||||
}
|
||||
|
||||
func ensureDefaults() {
|
||||
if Version == "" {
|
||||
Version = defaultVersion
|
||||
}
|
||||
if GoVersion == "" {
|
||||
GoVersion = runtime.Version()
|
||||
}
|
||||
}
|
||||
|
||||
func buildSettings(settings []debug.BuildSetting) map[string]string {
|
||||
values := make(map[string]string, len(settings))
|
||||
for _, setting := range settings {
|
||||
values[setting.Key] = setting.Value
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func normalizeVersion(version string) string {
|
||||
if version == "" || version == "(devel)" {
|
||||
return defaultVersion
|
||||
}
|
||||
if canonical := canonicalSemver(version); canonical != "" {
|
||||
return canonical
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
func canonicalSemver(version string) string {
|
||||
version = strings.TrimSpace(version)
|
||||
if version == "" || version == "(devel)" {
|
||||
return ""
|
||||
}
|
||||
if !strings.HasPrefix(version, "v") {
|
||||
version = "v" + version
|
||||
}
|
||||
return semver.Canonical(version)
|
||||
}
|
||||
|
||||
// SemanticVersion - Get the structured sematic version
|
||||
func SemanticVersion() []int {
|
||||
semVer := make([]int, 3)
|
||||
canonical := canonicalSemver(Version)
|
||||
if canonical == "" {
|
||||
return semVer
|
||||
}
|
||||
base := strings.TrimPrefix(canonical, "v")
|
||||
if idx := strings.Index(base, "-"); idx != -1 {
|
||||
base = base[:idx]
|
||||
}
|
||||
parts := strings.Split(base, ".")
|
||||
for i := 0; i < 3 && i < len(parts); i++ {
|
||||
semVer[i], _ = strconv.Atoi(parts[i])
|
||||
}
|
||||
return semVer
|
||||
}
|
||||
|
||||
// Compiled - Get time this binary was compiled
|
||||
func Compiled() (time.Time, error) {
|
||||
if CompiledAt == "" {
|
||||
return time.Unix(0, 0), errors.New("compiled time not available")
|
||||
}
|
||||
compiled, err := strconv.ParseInt(CompiledAt, 10, 64)
|
||||
if err != nil {
|
||||
return time.Unix(0, 0), err
|
||||
}
|
||||
return time.Unix(compiled, 0), nil
|
||||
}
|
||||
|
||||
// FullVersion - Full version string
|
||||
func FullVersion() string {
|
||||
ver := fmt.Sprintf("%s", Version)
|
||||
compiled, err := Compiled()
|
||||
if err == nil {
|
||||
ver += fmt.Sprintf(" - Compiled %s", compiled.String())
|
||||
}
|
||||
if GitCommit != "" {
|
||||
ver += fmt.Sprintf(" - %s", GitCommit)
|
||||
}
|
||||
return ver
|
||||
}
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
# Contributing to Survey
|
||||
|
||||
🎉🎉 First off, thanks for the interest in contributing to `survey`! 🎉🎉
|
||||
|
||||
The following is a set of guidelines to follow when contributing to this package. These are not hard rules, please use common sense and feel free to propose changes to this document in a pull request.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project and its contibutors are expected to uphold the [Go Community Code of Conduct](https://golang.org/conduct). By participating, you are expected to follow these guidelines.
|
||||
|
||||
## Getting help
|
||||
|
||||
* [Open an issue](https://github.com/AlecAivazis/survey/issues/new/choose)
|
||||
* Reach out to `@AlecAivazis` or `@mislav` in the Gophers slack (please use only when urgent)
|
||||
|
||||
## Submitting a contribution
|
||||
|
||||
When submitting a contribution,
|
||||
|
||||
- Try to make a series of smaller changes instead of one large change
|
||||
- Provide a description of each change that you are proposing
|
||||
- Reference the issue addressed by your pull request (if there is one)
|
||||
- Document all new exported Go APIs
|
||||
- Update the project's README when applicable
|
||||
- Include unit tests if possible
|
||||
- Contributions with visual ramifications or interaction changes should be accompanied with an integration test—see below for details.
|
||||
|
||||
## Writing and running tests
|
||||
|
||||
When submitting features, please add as many units tests as necessary to test both positive and negative cases.
|
||||
|
||||
Integration tests for survey uses [go-expect](https://github.com/Netflix/go-expect) to expect a match on stdout and respond on stdin. Since `os.Stdout` in a `go test` process is not a TTY, you need a way to interpret terminal / ANSI escape sequences for things like `CursorLocation`. The stdin/stdout handled by `go-expect` is also multiplexed to a [virtual terminal](https://github.com/hinshun/vt10x).
|
||||
|
||||
For example, you can extend the tests for Input by specifying the following test case:
|
||||
|
||||
```go
|
||||
{
|
||||
"Test Input prompt interaction", // Name of the test.
|
||||
&Input{ // An implementation of the survey.Prompt interface.
|
||||
Message: "What is your name?",
|
||||
},
|
||||
func(c *expect.Console) { // An expect procedure. You can expect strings / regexps and
|
||||
c.ExpectString("What is your name?") // write back strings / bytes to its psuedoterminal for survey.
|
||||
c.SendLine("Johnny Appleseed")
|
||||
c.ExpectEOF() // Nothing is read from the tty without an expect, and once an
|
||||
// expectation is met, no further bytes are read. End your
|
||||
// procedure with `c.ExpectEOF()` to read until survey finishes.
|
||||
},
|
||||
"Johnny Appleseed", // The expected result.
|
||||
}
|
||||
```
|
||||
|
||||
If you want to write your own `go-expect` test from scratch, you'll need to instantiate a virtual terminal,
|
||||
multiplex it into an `*expect.Console`, and hook up its tty with survey's optional stdio. Please see `go-expect`
|
||||
[documentation](https://godoc.org/github.com/Netflix/go-expect) for more detail.
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Alec Aivazis
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
-510
@@ -1,510 +0,0 @@
|
||||
# Survey
|
||||
|
||||
[](https://pkg.go.dev/github.com/AlecAivazis/survey/v2)
|
||||
|
||||
A library for building interactive and accessible prompts on terminals supporting ANSI escape sequences.
|
||||
|
||||
<img width="550" src="https://thumbs.gfycat.com/VillainousGraciousKouprey-size_restricted.gif"/>
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
)
|
||||
|
||||
// the questions to ask
|
||||
var qs = []*survey.Question{
|
||||
{
|
||||
Name: "name",
|
||||
Prompt: &survey.Input{Message: "What is your name?"},
|
||||
Validate: survey.Required,
|
||||
Transform: survey.Title,
|
||||
},
|
||||
{
|
||||
Name: "color",
|
||||
Prompt: &survey.Select{
|
||||
Message: "Choose a color:",
|
||||
Options: []string{"red", "blue", "green"},
|
||||
Default: "red",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "age",
|
||||
Prompt: &survey.Input{Message: "How old are you?"},
|
||||
},
|
||||
}
|
||||
|
||||
func main() {
|
||||
// the answers will be written to this struct
|
||||
answers := struct {
|
||||
Name string // survey will match the question and field names
|
||||
FavoriteColor string `survey:"color"` // or you can tag fields to match a specific name
|
||||
Age int // if the types don't match, survey will convert it
|
||||
}{}
|
||||
|
||||
// perform the questions
|
||||
err := survey.Ask(qs, &answers)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("%s chose %s.", answers.Name, answers.FavoriteColor)
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
Examples can be found in the `examples/` directory. Run them
|
||||
to see basic behavior:
|
||||
|
||||
```bash
|
||||
go run examples/simple.go
|
||||
go run examples/validation.go
|
||||
```
|
||||
|
||||
## Running the Prompts
|
||||
|
||||
There are two primary ways to execute prompts and start collecting information from your users: `Ask` and
|
||||
`AskOne`. The primary difference is whether you are interested in collecting a single piece of information
|
||||
or if you have a list of questions to ask whose answers should be collected in a single struct.
|
||||
For most basic usecases, `Ask` should be enough. However, for surveys with complicated branching logic,
|
||||
we recommend that you break out your questions into multiple calls to both of these functions to fit your needs.
|
||||
|
||||
### Configuring the Prompts
|
||||
|
||||
Most prompts take fine-grained configuration through fields on the structs you instantiate. It is also
|
||||
possible to change survey's default behaviors by passing `AskOpts` to either `Ask` or `AskOne`. Examples
|
||||
in this document will do both interchangeably:
|
||||
|
||||
```golang
|
||||
prompt := &Select{
|
||||
Message: "Choose a color:",
|
||||
Options: []string{"red", "blue", "green"},
|
||||
// can pass a validator directly
|
||||
Validate: survey.Required,
|
||||
}
|
||||
|
||||
// or define a default for the single call to `AskOne`
|
||||
// the answer will get written to the color variable
|
||||
survey.AskOne(prompt, &color, survey.WithValidator(survey.Required))
|
||||
|
||||
// or define a default for every entry in a list of questions
|
||||
// the answer will get copied into the matching field of the struct as shown above
|
||||
survey.Ask(questions, &answers, survey.WithValidator(survey.Required))
|
||||
```
|
||||
|
||||
## Prompts
|
||||
|
||||
### Input
|
||||
|
||||
<img src="https://thumbs.gfycat.com/LankyBlindAmericanpainthorse-size_restricted.gif" width="400px"/>
|
||||
|
||||
```golang
|
||||
name := ""
|
||||
prompt := &survey.Input{
|
||||
Message: "ping",
|
||||
}
|
||||
survey.AskOne(prompt, &name)
|
||||
```
|
||||
|
||||
#### Suggestion Options
|
||||
|
||||
<img src="https://i.imgur.com/Q7POpA1.gif" width="800px"/>
|
||||
|
||||
```golang
|
||||
file := ""
|
||||
prompt := &survey.Input{
|
||||
Message: "inform a file to save:",
|
||||
Suggest: func (toComplete string) []string {
|
||||
files, _ := filepath.Glob(toComplete + "*")
|
||||
return files
|
||||
},
|
||||
}
|
||||
}
|
||||
survey.AskOne(prompt, &file)
|
||||
```
|
||||
|
||||
### Multiline
|
||||
|
||||
<img src="https://thumbs.gfycat.com/ImperfectShimmeringBeagle-size_restricted.gif" width="400px"/>
|
||||
|
||||
```golang
|
||||
text := ""
|
||||
prompt := &survey.Multiline{
|
||||
Message: "ping",
|
||||
}
|
||||
survey.AskOne(prompt, &text)
|
||||
```
|
||||
|
||||
### Password
|
||||
|
||||
<img src="https://thumbs.gfycat.com/CompassionateSevereHypacrosaurus-size_restricted.gif" width="400px" />
|
||||
|
||||
```golang
|
||||
password := ""
|
||||
prompt := &survey.Password{
|
||||
Message: "Please type your password",
|
||||
}
|
||||
survey.AskOne(prompt, &password)
|
||||
```
|
||||
|
||||
### Confirm
|
||||
|
||||
<img src="https://thumbs.gfycat.com/UnkemptCarefulGermanpinscher-size_restricted.gif" width="400px"/>
|
||||
|
||||
```golang
|
||||
name := false
|
||||
prompt := &survey.Confirm{
|
||||
Message: "Do you like pie?",
|
||||
}
|
||||
survey.AskOne(prompt, &name)
|
||||
```
|
||||
|
||||
### Select
|
||||
|
||||
<img src="https://thumbs.gfycat.com/GrimFilthyAmazonparrot-size_restricted.gif" width="450px"/>
|
||||
|
||||
```golang
|
||||
color := ""
|
||||
prompt := &survey.Select{
|
||||
Message: "Choose a color:",
|
||||
Options: []string{"red", "blue", "green"},
|
||||
}
|
||||
survey.AskOne(prompt, &color)
|
||||
```
|
||||
|
||||
Fields and values that come from a `Select` prompt can be one of two different things. If you pass an `int`
|
||||
the field will have the value of the selected index. If you instead pass a string, the string value selected
|
||||
will be written to the field.
|
||||
|
||||
The user can also press `esc` to toggle the ability cycle through the options with the j and k keys to do down and up respectively.
|
||||
|
||||
By default, the select prompt is limited to showing 7 options at a time
|
||||
and will paginate lists of options longer than that. This can be changed a number of ways:
|
||||
|
||||
```golang
|
||||
// as a field on a single select
|
||||
prompt := &survey.MultiSelect{..., PageSize: 10}
|
||||
|
||||
// or as an option to Ask or AskOne
|
||||
survey.AskOne(prompt, &days, survey.WithPageSize(10))
|
||||
```
|
||||
|
||||
#### Select options description
|
||||
|
||||
The optional description text can be used to add extra information to each option listed in the select prompt:
|
||||
|
||||
```golang
|
||||
color := ""
|
||||
prompt := &survey.Select{
|
||||
Message: "Choose a color:",
|
||||
Options: []string{"red", "blue", "green"},
|
||||
Description: func(value string, index int) string {
|
||||
if value == "red" {
|
||||
return "My favorite color"
|
||||
}
|
||||
return ""
|
||||
},
|
||||
}
|
||||
survey.AskOne(prompt, &color)
|
||||
|
||||
// Assuming that the user chose "red - My favorite color":
|
||||
fmt.Println(color) //=> "red"
|
||||
```
|
||||
|
||||
### MultiSelect
|
||||
|
||||

|
||||
|
||||
```golang
|
||||
days := []string{}
|
||||
prompt := &survey.MultiSelect{
|
||||
Message: "What days do you prefer:",
|
||||
Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"},
|
||||
}
|
||||
survey.AskOne(prompt, &days)
|
||||
```
|
||||
|
||||
Fields and values that come from a `MultiSelect` prompt can be one of two different things. If you pass an `int`
|
||||
the field will have a slice of the selected indices. If you instead pass a string, a slice of the string values
|
||||
selected will be written to the field.
|
||||
|
||||
The user can also press `esc` to toggle the ability cycle through the options with the j and k keys to do down and up respectively.
|
||||
|
||||
By default, the MultiSelect prompt is limited to showing 7 options at a time
|
||||
and will paginate lists of options longer than that. This can be changed a number of ways:
|
||||
|
||||
```golang
|
||||
// as a field on a single select
|
||||
prompt := &survey.MultiSelect{..., PageSize: 10}
|
||||
|
||||
// or as an option to Ask or AskOne
|
||||
survey.AskOne(prompt, &days, survey.WithPageSize(10))
|
||||
```
|
||||
|
||||
### Editor
|
||||
|
||||
Launches the user's preferred editor (defined by the \$VISUAL or \$EDITOR environment variables) on a
|
||||
temporary file. Once the user exits their editor, the contents of the temporary file are read in as
|
||||
the result. If neither of those are present, notepad (on Windows) or vim (Linux or Mac) is used.
|
||||
|
||||
You can also specify a [pattern](https://golang.org/pkg/io/ioutil/#TempFile) for the name of the temporary file. This
|
||||
can be useful for ensuring syntax highlighting matches your usecase.
|
||||
|
||||
```golang
|
||||
prompt := &survey.Editor{
|
||||
Message: "Shell code snippet",
|
||||
FileName: "*.sh",
|
||||
}
|
||||
|
||||
survey.AskOne(prompt, &content)
|
||||
```
|
||||
|
||||
## Filtering Options
|
||||
|
||||
By default, the user can filter for options in Select and MultiSelects by typing while the prompt
|
||||
is active. This will filter out all options that don't contain the typed string anywhere in their name, ignoring case.
|
||||
|
||||
A custom filter function can also be provided to change this behavior:
|
||||
|
||||
```golang
|
||||
func myFilter(filterValue string, optValue string, optIndex int) bool {
|
||||
// only include the option if it includes the filter and has length greater than 5
|
||||
return strings.Contains(optValue, filterValue) && len(optValue) >= 5
|
||||
}
|
||||
|
||||
// configure it for a specific prompt
|
||||
&Select{
|
||||
Message: "Choose a color:",
|
||||
Options: []string{"red", "blue", "green"},
|
||||
Filter: myFilter,
|
||||
}
|
||||
|
||||
// or define a default for all of the questions
|
||||
survey.AskOne(prompt, &color, survey.WithFilter(myFilter))
|
||||
```
|
||||
|
||||
## Keeping the filter active
|
||||
|
||||
By default the filter will disappear if the user selects one of the filtered elements. Once the user selects one element the filter setting is gone.
|
||||
|
||||
However the user can prevent this from happening and keep the filter active for multiple selections in a e.g. MultiSelect:
|
||||
|
||||
```golang
|
||||
// configure it for a specific prompt
|
||||
&Select{
|
||||
Message: "Choose a color:",
|
||||
Options: []string{"light-green", "green", "dark-green", "red"},
|
||||
KeepFilter: true,
|
||||
}
|
||||
|
||||
// or define a default for all of the questions
|
||||
survey.AskOne(prompt, &color, survey.WithKeepFilter(true))
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
Validating individual responses for a particular question can be done by defining a
|
||||
`Validate` field on the `survey.Question` to be validated. This function takes an
|
||||
`interface{}` type and returns an error to show to the user, prompting them for another
|
||||
response. Like usual, validators can be provided directly to the prompt or with `survey.WithValidator`:
|
||||
|
||||
```golang
|
||||
q := &survey.Question{
|
||||
Prompt: &survey.Input{Message: "Hello world validation"},
|
||||
Validate: func (val interface{}) error {
|
||||
// since we are validating an Input, the assertion will always succeed
|
||||
if str, ok := val.(string) ; !ok || len(str) > 10 {
|
||||
return errors.New("This response cannot be longer than 10 characters.")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
color := ""
|
||||
prompt := &survey.Input{ Message: "Whats your name?" }
|
||||
|
||||
// you can pass multiple validators here and survey will make sure each one passes
|
||||
survey.AskOne(prompt, &color, survey.WithValidator(survey.Required))
|
||||
```
|
||||
|
||||
### Built-in Validators
|
||||
|
||||
`survey` comes prepackaged with a few validators to fit common situations. Currently these
|
||||
validators include:
|
||||
|
||||
| name | valid types | description | notes |
|
||||
| ------------ | -------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
||||
| Required | any | Rejects zero values of the response type | Boolean values pass straight through since the zero value (false) is a valid response |
|
||||
| MinLength(n) | string | Enforces that a response is at least the given length | |
|
||||
| MaxLength(n) | string | Enforces that a response is no longer than the given length | |
|
||||
| MaxItems(n) | []OptionAnswer | Enforces that a response has no more selections of the indicated | |
|
||||
| MinItems(n) | []OptionAnswer | Enforces that a response has no less selections of the indicated | |
|
||||
|
||||
## Help Text
|
||||
|
||||
All of the prompts have a `Help` field which can be defined to provide more information to your users:
|
||||
|
||||
<img src="https://thumbs.gfycat.com/CloudyRemorsefulFossa-size_restricted.gif" width="400px" style="margin-top: 8px"/>
|
||||
|
||||
```golang
|
||||
&survey.Input{
|
||||
Message: "What is your phone number:",
|
||||
Help: "Phone number should include the area code",
|
||||
}
|
||||
```
|
||||
|
||||
## Removing the "Select All" and "Select None" options
|
||||
|
||||
By default, users can select all of the multi-select options using the right arrow key. To prevent users from being able to do this (and remove the `<right> to all` message from the prompt), use the option `WithRemoveSelectAll`:
|
||||
|
||||
```golang
|
||||
import (
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
)
|
||||
|
||||
number := ""
|
||||
prompt := &survey.Input{
|
||||
Message: "This question has the select all option removed",
|
||||
}
|
||||
|
||||
survey.AskOne(prompt, &number, survey.WithRemoveSelectAll())
|
||||
```
|
||||
|
||||
Also by default, users can use the left arrow key to unselect all of the options. To prevent users from being able to do this (and remove the `<left> to none` message from the prompt), use the option `WithRemoveSelectNone`:
|
||||
|
||||
```golang
|
||||
import (
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
)
|
||||
|
||||
number := ""
|
||||
prompt := &survey.Input{
|
||||
Message: "This question has the select all option removed",
|
||||
}
|
||||
|
||||
survey.AskOne(prompt, &number, survey.WithRemoveSelectNone())
|
||||
```
|
||||
|
||||
|
||||
### Changing the input rune
|
||||
|
||||
In some situations, `?` is a perfectly valid response. To handle this, you can change the rune that survey
|
||||
looks for with `WithHelpInput`:
|
||||
|
||||
```golang
|
||||
import (
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
)
|
||||
|
||||
number := ""
|
||||
prompt := &survey.Input{
|
||||
Message: "If you have this need, please give me a reasonable message.",
|
||||
Help: "I couldn't come up with one.",
|
||||
}
|
||||
|
||||
survey.AskOne(prompt, &number, survey.WithHelpInput('^'))
|
||||
```
|
||||
|
||||
## Changing the Icons
|
||||
|
||||
Changing the icons and their color/format can be done by passing the `WithIcons` option. The format
|
||||
follows the patterns outlined [here](https://github.com/mgutz/ansi#style-format). For example:
|
||||
|
||||
```golang
|
||||
import (
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
)
|
||||
|
||||
number := ""
|
||||
prompt := &survey.Input{
|
||||
Message: "If you have this need, please give me a reasonable message.",
|
||||
Help: "I couldn't come up with one.",
|
||||
}
|
||||
|
||||
survey.AskOne(prompt, &number, survey.WithIcons(func(icons *survey.IconSet) {
|
||||
// you can set any icons
|
||||
icons.Question.Text = "⁇"
|
||||
// for more information on formatting the icons, see here: https://github.com/mgutz/ansi#style-format
|
||||
icons.Question.Format = "yellow+hb"
|
||||
}))
|
||||
```
|
||||
|
||||
The icons and their default text and format are summarized below:
|
||||
|
||||
| name | text | format | description |
|
||||
| -------------- | ---- | ---------- | ------------------------------------------------------------- |
|
||||
| Error | X | red | Before an error |
|
||||
| Help | i | cyan | Before help text |
|
||||
| Question | ? | green+hb | Before the message of a prompt |
|
||||
| SelectFocus | > | green | Marks the current focus in `Select` and `MultiSelect` prompts |
|
||||
| UnmarkedOption | [ ] | default+hb | Marks an unselected option in a `MultiSelect` prompt |
|
||||
| MarkedOption | [x] | cyan+b | Marks a chosen selection in a `MultiSelect` prompt |
|
||||
|
||||
## Custom Types
|
||||
|
||||
survey will assign prompt answers to your custom types if they implement this interface:
|
||||
|
||||
```golang
|
||||
type Settable interface {
|
||||
WriteAnswer(field string, value interface{}) error
|
||||
}
|
||||
```
|
||||
|
||||
Here is an example how to use them:
|
||||
|
||||
```golang
|
||||
type MyValue struct {
|
||||
value string
|
||||
}
|
||||
func (my *MyValue) WriteAnswer(name string, value interface{}) error {
|
||||
my.value = value.(string)
|
||||
}
|
||||
|
||||
myval := MyValue{}
|
||||
survey.AskOne(
|
||||
&survey.Input{
|
||||
Message: "Enter something:",
|
||||
},
|
||||
&myval
|
||||
)
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
You can test your program's interactive prompts using [go-expect](https://github.com/Netflix/go-expect). The library
|
||||
can be used to expect a match on stdout and respond on stdin. Since `os.Stdout` in a `go test` process is not a TTY,
|
||||
if you are manipulating the cursor or using `survey`, you will need a way to interpret terminal / ANSI escape sequences
|
||||
for things like `CursorLocation`. `vt10x.NewVT10XConsole` will create a `go-expect` console that also multiplexes
|
||||
stdio to an in-memory [virtual terminal](https://github.com/hinshun/vt10x).
|
||||
|
||||
For some examples, you can see any of the tests in this repo.
|
||||
|
||||
## FAQ
|
||||
|
||||
### What kinds of IO are supported by `survey`?
|
||||
|
||||
survey aims to support most terminal emulators; it expects support for ANSI escape sequences.
|
||||
This means that reading from piped stdin or writing to piped stdout is **not supported**,
|
||||
and likely to break your application in these situations. See [#337](https://github.com/AlecAivazis/survey/pull/337#issue-581351617)
|
||||
|
||||
### Why isn't Ctrl-C working?
|
||||
|
||||
Ordinarily, when you type Ctrl-C, the terminal recognizes this as the QUIT button and delivers a SIGINT signal to the process, which terminates it.
|
||||
However, Survey temporarily configures the terminal to deliver control codes as ordinary input bytes.
|
||||
When Survey reads a ^C byte (ASCII \x03, "end of text"), it interrupts the current survey and returns a
|
||||
`github.com/AlecAivazis/survey/v2/terminal.InterruptErr` from `Ask` or `AskOne`.
|
||||
If you want to stop the process, handle the returned error in your code:
|
||||
|
||||
```go
|
||||
err := survey.AskOne(prompt, &myVar)
|
||||
if err != nil {
|
||||
if err == terminal.InterruptErr {
|
||||
log.Fatal("interrupted")
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
-154
@@ -1,154 +0,0 @@
|
||||
package survey
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// Confirm is a regular text input that accept yes/no answers. Response type is a bool.
|
||||
type Confirm struct {
|
||||
Renderer
|
||||
Message string
|
||||
Default bool
|
||||
Help string
|
||||
}
|
||||
|
||||
// data available to the templates when processing
|
||||
type ConfirmTemplateData struct {
|
||||
Confirm
|
||||
Answer string
|
||||
ShowHelp bool
|
||||
Config *PromptConfig
|
||||
}
|
||||
|
||||
// Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format
|
||||
var ConfirmQuestionTemplate = `
|
||||
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
|
||||
{{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}}
|
||||
{{- color "default+hb"}}{{ .Message }} {{color "reset"}}
|
||||
{{- if .Answer}}
|
||||
{{- color "cyan"}}{{.Answer}}{{color "reset"}}{{"\n"}}
|
||||
{{- else }}
|
||||
{{- if and .Help (not .ShowHelp)}}{{color "cyan"}}[{{ .Config.HelpInput }} for help]{{color "reset"}} {{end}}
|
||||
{{- color "white"}}{{if .Default}}(Y/n) {{else}}(y/N) {{end}}{{color "reset"}}
|
||||
{{- end}}`
|
||||
|
||||
// the regex for answers
|
||||
var (
|
||||
yesRx = regexp.MustCompile("^(?i:y(?:es)?)$")
|
||||
noRx = regexp.MustCompile("^(?i:n(?:o)?)$")
|
||||
)
|
||||
|
||||
func yesNo(t bool) string {
|
||||
if t {
|
||||
return "Yes"
|
||||
}
|
||||
return "No"
|
||||
}
|
||||
|
||||
func (c *Confirm) getBool(showHelp bool, config *PromptConfig) (bool, error) {
|
||||
cursor := c.NewCursor()
|
||||
rr := c.NewRuneReader()
|
||||
_ = rr.SetTermMode()
|
||||
defer func() {
|
||||
_ = rr.RestoreTermMode()
|
||||
}()
|
||||
|
||||
// start waiting for input
|
||||
for {
|
||||
line, err := rr.ReadLine(0)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
// move back up a line to compensate for the \n echoed from terminal
|
||||
cursor.PreviousLine(1)
|
||||
val := string(line)
|
||||
|
||||
// get the answer that matches the
|
||||
var answer bool
|
||||
switch {
|
||||
case yesRx.Match([]byte(val)):
|
||||
answer = true
|
||||
case noRx.Match([]byte(val)):
|
||||
answer = false
|
||||
case val == "":
|
||||
answer = c.Default
|
||||
case val == config.HelpInput && c.Help != "":
|
||||
err := c.Render(
|
||||
ConfirmQuestionTemplate,
|
||||
ConfirmTemplateData{
|
||||
Confirm: *c,
|
||||
ShowHelp: true,
|
||||
Config: config,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
// use the default value and bubble up
|
||||
return c.Default, err
|
||||
}
|
||||
showHelp = true
|
||||
continue
|
||||
default:
|
||||
// we didnt get a valid answer, so print error and prompt again
|
||||
//lint:ignore ST1005 it should be fine for this error message to have punctuation
|
||||
if err := c.Error(config, fmt.Errorf("%q is not a valid answer, please try again.", val)); err != nil {
|
||||
return c.Default, err
|
||||
}
|
||||
err := c.Render(
|
||||
ConfirmQuestionTemplate,
|
||||
ConfirmTemplateData{
|
||||
Confirm: *c,
|
||||
ShowHelp: showHelp,
|
||||
Config: config,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
// use the default value and bubble up
|
||||
return c.Default, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
return answer, nil
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Prompt prompts the user with a simple text field and expects a reply followed
|
||||
by a carriage return.
|
||||
|
||||
likesPie := false
|
||||
prompt := &survey.Confirm{ Message: "What is your name?" }
|
||||
survey.AskOne(prompt, &likesPie)
|
||||
*/
|
||||
func (c *Confirm) Prompt(config *PromptConfig) (interface{}, error) {
|
||||
// render the question template
|
||||
err := c.Render(
|
||||
ConfirmQuestionTemplate,
|
||||
ConfirmTemplateData{
|
||||
Confirm: *c,
|
||||
Config: config,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// get input and return
|
||||
return c.getBool(false, config)
|
||||
}
|
||||
|
||||
// Cleanup overwrite the line with the finalized formatted version
|
||||
func (c *Confirm) Cleanup(config *PromptConfig, val interface{}) error {
|
||||
// if the value was previously true
|
||||
ans := yesNo(val.(bool))
|
||||
|
||||
// render the template
|
||||
return c.Render(
|
||||
ConfirmQuestionTemplate,
|
||||
ConfirmTemplateData{
|
||||
Confirm: *c,
|
||||
Answer: ans,
|
||||
Config: config,
|
||||
},
|
||||
)
|
||||
}
|
||||
-104
@@ -1,104 +0,0 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"sync"
|
||||
"text/template"
|
||||
|
||||
"github.com/mgutz/ansi"
|
||||
)
|
||||
|
||||
// DisableColor can be used to make testing reliable
|
||||
var DisableColor = false
|
||||
|
||||
var TemplateFuncsWithColor = map[string]interface{}{
|
||||
// Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format
|
||||
"color": ansi.ColorCode,
|
||||
}
|
||||
|
||||
var TemplateFuncsNoColor = map[string]interface{}{
|
||||
// Templates without Color formatting. For layout/ testing.
|
||||
"color": func(color string) string {
|
||||
return ""
|
||||
},
|
||||
}
|
||||
|
||||
// envColorDisabled returns if output colors are forbid by environment variables
|
||||
func envColorDisabled() bool {
|
||||
return os.Getenv("NO_COLOR") != "" || os.Getenv("CLICOLOR") == "0"
|
||||
}
|
||||
|
||||
// envColorForced returns if output colors are forced from environment variables
|
||||
func envColorForced() bool {
|
||||
val, ok := os.LookupEnv("CLICOLOR_FORCE")
|
||||
return ok && val != "0"
|
||||
}
|
||||
|
||||
// RunTemplate returns two formatted strings given a template and
|
||||
// the data it requires. The first string returned is generated for
|
||||
// user-facing output and may or may not contain ANSI escape codes
|
||||
// for colored output. The second string does not contain escape codes
|
||||
// and can be used by the renderer for layout purposes.
|
||||
func RunTemplate(tmpl string, data interface{}) (string, string, error) {
|
||||
tPair, err := GetTemplatePair(tmpl)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
userBuf := bytes.NewBufferString("")
|
||||
err = tPair[0].Execute(userBuf, data)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
layoutBuf := bytes.NewBufferString("")
|
||||
err = tPair[1].Execute(layoutBuf, data)
|
||||
if err != nil {
|
||||
return userBuf.String(), "", err
|
||||
}
|
||||
return userBuf.String(), layoutBuf.String(), err
|
||||
}
|
||||
|
||||
var (
|
||||
memoizedGetTemplate = map[string][2]*template.Template{}
|
||||
|
||||
memoMutex = &sync.RWMutex{}
|
||||
)
|
||||
|
||||
// GetTemplatePair returns a pair of compiled templates where the
|
||||
// first template is generated for user-facing output and the
|
||||
// second is generated for use by the renderer. The second
|
||||
// template does not contain any color escape codes, whereas
|
||||
// the first template may or may not depending on DisableColor.
|
||||
func GetTemplatePair(tmpl string) ([2]*template.Template, error) {
|
||||
memoMutex.RLock()
|
||||
if t, ok := memoizedGetTemplate[tmpl]; ok {
|
||||
memoMutex.RUnlock()
|
||||
return t, nil
|
||||
}
|
||||
memoMutex.RUnlock()
|
||||
|
||||
templatePair := [2]*template.Template{nil, nil}
|
||||
|
||||
templateNoColor, err := template.New("prompt").Funcs(TemplateFuncsNoColor).Parse(tmpl)
|
||||
if err != nil {
|
||||
return [2]*template.Template{}, err
|
||||
}
|
||||
|
||||
templatePair[1] = templateNoColor
|
||||
|
||||
envColorHide := envColorDisabled() && !envColorForced()
|
||||
if DisableColor || envColorHide {
|
||||
templatePair[0] = templatePair[1]
|
||||
} else {
|
||||
templateWithColor, err := template.New("prompt").Funcs(TemplateFuncsWithColor).Parse(tmpl)
|
||||
templatePair[0] = templateWithColor
|
||||
if err != nil {
|
||||
return [2]*template.Template{}, err
|
||||
}
|
||||
}
|
||||
|
||||
memoMutex.Lock()
|
||||
memoizedGetTemplate[tmpl] = templatePair
|
||||
memoMutex.Unlock()
|
||||
return templatePair, nil
|
||||
}
|
||||
-376
@@ -1,376 +0,0 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// the tag used to denote the name of the question
|
||||
const tagName = "survey"
|
||||
|
||||
// Settable allow for configuration when assigning answers
|
||||
type Settable interface {
|
||||
WriteAnswer(field string, value interface{}) error
|
||||
}
|
||||
|
||||
// OptionAnswer is the return type of Selects/MultiSelects that lets the appropriate information
|
||||
// get copied to the user's struct
|
||||
type OptionAnswer struct {
|
||||
Value string
|
||||
Index int
|
||||
}
|
||||
|
||||
type reflectField struct {
|
||||
value reflect.Value
|
||||
fieldType reflect.StructField
|
||||
}
|
||||
|
||||
func OptionAnswerList(incoming []string) []OptionAnswer {
|
||||
list := []OptionAnswer{}
|
||||
for i, opt := range incoming {
|
||||
list = append(list, OptionAnswer{Value: opt, Index: i})
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
func WriteAnswer(t interface{}, name string, v interface{}) (err error) {
|
||||
// if the field is a custom type
|
||||
if s, ok := t.(Settable); ok {
|
||||
// use the interface method
|
||||
return s.WriteAnswer(name, v)
|
||||
}
|
||||
|
||||
// the target to write to
|
||||
target := reflect.ValueOf(t)
|
||||
// the value to write from
|
||||
value := reflect.ValueOf(v)
|
||||
|
||||
// make sure we are writing to a pointer
|
||||
if target.Kind() != reflect.Ptr {
|
||||
return errors.New("you must pass a pointer as the target of a Write operation")
|
||||
}
|
||||
// the object "inside" of the target pointer
|
||||
elem := target.Elem()
|
||||
|
||||
// handle the special types
|
||||
switch elem.Kind() {
|
||||
// if we are writing to a struct
|
||||
case reflect.Struct:
|
||||
// if we are writing to an option answer than we want to treat
|
||||
// it like a single thing and not a place to deposit answers
|
||||
if elem.Type().Name() == "OptionAnswer" {
|
||||
// copy the value over to the normal struct
|
||||
return copy(elem, value)
|
||||
}
|
||||
|
||||
// get the name of the field that matches the string we were given
|
||||
field, _, err := findField(elem, name)
|
||||
// if something went wrong
|
||||
if err != nil {
|
||||
// bubble up
|
||||
return err
|
||||
}
|
||||
// handle references to the Settable interface aswell
|
||||
if s, ok := field.Interface().(Settable); ok {
|
||||
// use the interface method
|
||||
return s.WriteAnswer(name, v)
|
||||
}
|
||||
if field.CanAddr() {
|
||||
if s, ok := field.Addr().Interface().(Settable); ok {
|
||||
// use the interface method
|
||||
return s.WriteAnswer(name, v)
|
||||
}
|
||||
}
|
||||
|
||||
// copy the value over to the normal struct
|
||||
return copy(field, value)
|
||||
case reflect.Map:
|
||||
mapType := reflect.TypeOf(t).Elem()
|
||||
if mapType.Key().Kind() != reflect.String {
|
||||
return errors.New("answer maps key must be of type string")
|
||||
}
|
||||
|
||||
// copy only string value/index value to map if,
|
||||
// map is not of type interface and is 'OptionAnswer'
|
||||
if value.Type().Name() == "OptionAnswer" {
|
||||
if kval := mapType.Elem().Kind(); kval == reflect.String {
|
||||
mt := *t.(*map[string]string)
|
||||
mt[name] = value.FieldByName("Value").String()
|
||||
return nil
|
||||
} else if kval == reflect.Int {
|
||||
mt := *t.(*map[string]int)
|
||||
mt[name] = int(value.FieldByName("Index").Int())
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if mapType.Elem().Kind() != reflect.Interface {
|
||||
return errors.New("answer maps must be of type map[string]interface")
|
||||
}
|
||||
mt := *t.(*map[string]interface{})
|
||||
mt[name] = value.Interface()
|
||||
return nil
|
||||
}
|
||||
// otherwise just copy the value to the target
|
||||
return copy(elem, value)
|
||||
}
|
||||
|
||||
type errFieldNotMatch struct {
|
||||
questionName string
|
||||
}
|
||||
|
||||
func (err errFieldNotMatch) Error() string {
|
||||
return fmt.Sprintf("could not find field matching %v", err.questionName)
|
||||
}
|
||||
|
||||
func (err errFieldNotMatch) Is(target error) bool { // implements the dynamic errors.Is interface.
|
||||
if target != nil {
|
||||
if name, ok := IsFieldNotMatch(target); ok {
|
||||
// if have a filled questionName then perform "deeper" comparison.
|
||||
return name == "" || err.questionName == "" || name == err.questionName
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// IsFieldNotMatch reports whether an "err" is caused by a non matching field.
|
||||
// It returns the Question.Name that couldn't be matched with a destination field.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// if err := survey.Ask(qs, &v); err != nil {
|
||||
// if name, ok := core.IsFieldNotMatch(err); ok {
|
||||
// // name is the question name that did not match a field
|
||||
// }
|
||||
// }
|
||||
func IsFieldNotMatch(err error) (string, bool) {
|
||||
if err != nil {
|
||||
if v, ok := err.(errFieldNotMatch); ok {
|
||||
return v.questionName, true
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// BUG(AlecAivazis): the current implementation might cause weird conflicts if there are
|
||||
// two fields with same name that only differ by casing.
|
||||
func findField(s reflect.Value, name string) (reflect.Value, reflect.StructField, error) {
|
||||
|
||||
fields := flattenFields(s)
|
||||
|
||||
// first look for matching tags so we can overwrite matching field names
|
||||
for _, f := range fields {
|
||||
// the value of the survey tag
|
||||
tag := f.fieldType.Tag.Get(tagName)
|
||||
// if the tag matches the name we are looking for
|
||||
if tag != "" && tag == name {
|
||||
// then we found our index
|
||||
return f.value, f.fieldType, nil
|
||||
}
|
||||
}
|
||||
|
||||
// then look for matching names
|
||||
for _, f := range fields {
|
||||
// if the name of the field matches what we're looking for
|
||||
if strings.EqualFold(f.fieldType.Name, name) {
|
||||
return f.value, f.fieldType, nil
|
||||
}
|
||||
}
|
||||
|
||||
// we didn't find the field
|
||||
return reflect.Value{}, reflect.StructField{}, errFieldNotMatch{name}
|
||||
}
|
||||
|
||||
func flattenFields(s reflect.Value) []reflectField {
|
||||
sType := s.Type()
|
||||
numField := sType.NumField()
|
||||
fields := make([]reflectField, 0, numField)
|
||||
for i := 0; i < numField; i++ {
|
||||
fieldType := sType.Field(i)
|
||||
field := s.Field(i)
|
||||
|
||||
if field.Kind() == reflect.Struct && fieldType.Anonymous {
|
||||
// field is a promoted structure
|
||||
fields = append(fields, flattenFields(field)...)
|
||||
continue
|
||||
}
|
||||
fields = append(fields, reflectField{field, fieldType})
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
// isList returns true if the element is something we can Len()
|
||||
func isList(v reflect.Value) bool {
|
||||
switch v.Type().Kind() {
|
||||
case reflect.Array, reflect.Slice:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Write takes a value and copies it to the target
|
||||
func copy(t reflect.Value, v reflect.Value) (err error) {
|
||||
// if something ends up panicing we need to catch it in a deferred func
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// if we paniced with an error
|
||||
if _, ok := r.(error); ok {
|
||||
// cast the result to an error object
|
||||
err = r.(error)
|
||||
} else if _, ok := r.(string); ok {
|
||||
// otherwise we could have paniced with a string so wrap it in an error
|
||||
err = errors.New(r.(string))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// if we are copying from a string result to something else
|
||||
if v.Kind() == reflect.String && v.Type() != t.Type() {
|
||||
var castVal interface{}
|
||||
var casterr error
|
||||
vString := v.Interface().(string)
|
||||
|
||||
switch t.Kind() {
|
||||
case reflect.Bool:
|
||||
castVal, casterr = strconv.ParseBool(vString)
|
||||
case reflect.Int:
|
||||
castVal, casterr = strconv.Atoi(vString)
|
||||
case reflect.Int8:
|
||||
var val64 int64
|
||||
val64, casterr = strconv.ParseInt(vString, 10, 8)
|
||||
if casterr == nil {
|
||||
castVal = int8(val64)
|
||||
}
|
||||
case reflect.Int16:
|
||||
var val64 int64
|
||||
val64, casterr = strconv.ParseInt(vString, 10, 16)
|
||||
if casterr == nil {
|
||||
castVal = int16(val64)
|
||||
}
|
||||
case reflect.Int32:
|
||||
var val64 int64
|
||||
val64, casterr = strconv.ParseInt(vString, 10, 32)
|
||||
if casterr == nil {
|
||||
castVal = int32(val64)
|
||||
}
|
||||
case reflect.Int64:
|
||||
if t.Type() == reflect.TypeOf(time.Duration(0)) {
|
||||
castVal, casterr = time.ParseDuration(vString)
|
||||
} else {
|
||||
castVal, casterr = strconv.ParseInt(vString, 10, 64)
|
||||
}
|
||||
case reflect.Uint:
|
||||
var val64 uint64
|
||||
val64, casterr = strconv.ParseUint(vString, 10, 8)
|
||||
if casterr == nil {
|
||||
castVal = uint(val64)
|
||||
}
|
||||
case reflect.Uint8:
|
||||
var val64 uint64
|
||||
val64, casterr = strconv.ParseUint(vString, 10, 8)
|
||||
if casterr == nil {
|
||||
castVal = uint8(val64)
|
||||
}
|
||||
case reflect.Uint16:
|
||||
var val64 uint64
|
||||
val64, casterr = strconv.ParseUint(vString, 10, 16)
|
||||
if casterr == nil {
|
||||
castVal = uint16(val64)
|
||||
}
|
||||
case reflect.Uint32:
|
||||
var val64 uint64
|
||||
val64, casterr = strconv.ParseUint(vString, 10, 32)
|
||||
if casterr == nil {
|
||||
castVal = uint32(val64)
|
||||
}
|
||||
case reflect.Uint64:
|
||||
castVal, casterr = strconv.ParseUint(vString, 10, 64)
|
||||
case reflect.Float32:
|
||||
var val64 float64
|
||||
val64, casterr = strconv.ParseFloat(vString, 32)
|
||||
if casterr == nil {
|
||||
castVal = float32(val64)
|
||||
}
|
||||
case reflect.Float64:
|
||||
castVal, casterr = strconv.ParseFloat(vString, 64)
|
||||
default:
|
||||
//lint:ignore ST1005 allow this error message to be capitalized
|
||||
return fmt.Errorf("Unable to convert from string to type %s", t.Kind())
|
||||
}
|
||||
|
||||
if casterr != nil {
|
||||
return casterr
|
||||
}
|
||||
|
||||
t.Set(reflect.ValueOf(castVal))
|
||||
return
|
||||
}
|
||||
|
||||
// if we are copying from an OptionAnswer to something
|
||||
if v.Type().Name() == "OptionAnswer" {
|
||||
// copying an option answer to a string
|
||||
if t.Kind() == reflect.String {
|
||||
// copies the Value field of the struct
|
||||
t.Set(reflect.ValueOf(v.FieldByName("Value").Interface()))
|
||||
return
|
||||
}
|
||||
|
||||
// copying an option answer to an int
|
||||
if t.Kind() == reflect.Int {
|
||||
// copies the Index field of the struct
|
||||
t.Set(reflect.ValueOf(v.FieldByName("Index").Interface()))
|
||||
return
|
||||
}
|
||||
|
||||
// copying an OptionAnswer to an OptionAnswer
|
||||
if t.Type().Name() == "OptionAnswer" {
|
||||
t.Set(v)
|
||||
return
|
||||
}
|
||||
|
||||
// we're copying an option answer to an incorrect type
|
||||
//lint:ignore ST1005 allow this error message to be capitalized
|
||||
return fmt.Errorf("Unable to convert from OptionAnswer to type %s", t.Kind())
|
||||
}
|
||||
|
||||
// if we are copying from one slice or array to another
|
||||
if isList(v) && isList(t) {
|
||||
// loop over every item in the desired value
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
// write to the target given its kind
|
||||
switch t.Kind() {
|
||||
// if its a slice
|
||||
case reflect.Slice:
|
||||
// an object of the correct type
|
||||
obj := reflect.Indirect(reflect.New(t.Type().Elem()))
|
||||
|
||||
// write the appropriate value to the obj and catch any errors
|
||||
if err := copy(obj, v.Index(i)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// just append the value to the end
|
||||
t.Set(reflect.Append(t, obj))
|
||||
// otherwise it could be an array
|
||||
case reflect.Array:
|
||||
// set the index to the appropriate value
|
||||
if err := copy(t.Slice(i, i+1).Index(0), v.Index(i)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// set the value to the target
|
||||
t.Set(v)
|
||||
}
|
||||
|
||||
// we're done
|
||||
return
|
||||
}
|
||||
-226
@@ -1,226 +0,0 @@
|
||||
package survey
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2/terminal"
|
||||
shellquote "github.com/kballard/go-shellquote"
|
||||
)
|
||||
|
||||
/*
|
||||
Editor launches an instance of the users preferred editor on a temporary file.
|
||||
The editor to use is determined by reading the $VISUAL or $EDITOR environment
|
||||
variables. If neither of those are present, notepad (on Windows) or vim
|
||||
(others) is used.
|
||||
The launch of the editor is triggered by the enter key. Since the response may
|
||||
be long, it will not be echoed as Input does, instead, it print <Received>.
|
||||
Response type is a string.
|
||||
|
||||
message := ""
|
||||
prompt := &survey.Editor{ Message: "What is your commit message?" }
|
||||
survey.AskOne(prompt, &message)
|
||||
*/
|
||||
type Editor struct {
|
||||
Renderer
|
||||
Message string
|
||||
Default string
|
||||
Help string
|
||||
Editor string
|
||||
HideDefault bool
|
||||
AppendDefault bool
|
||||
FileName string
|
||||
}
|
||||
|
||||
// data available to the templates when processing
|
||||
type EditorTemplateData struct {
|
||||
Editor
|
||||
Answer string
|
||||
ShowAnswer bool
|
||||
ShowHelp bool
|
||||
Config *PromptConfig
|
||||
}
|
||||
|
||||
// Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format
|
||||
var EditorQuestionTemplate = `
|
||||
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
|
||||
{{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}}
|
||||
{{- color "default+hb"}}{{ .Message }} {{color "reset"}}
|
||||
{{- if .ShowAnswer}}
|
||||
{{- color "cyan"}}{{.Answer}}{{color "reset"}}{{"\n"}}
|
||||
{{- else }}
|
||||
{{- if and .Help (not .ShowHelp)}}{{color "cyan"}}[{{ .Config.HelpInput }} for help]{{color "reset"}} {{end}}
|
||||
{{- if and .Default (not .HideDefault)}}{{color "white"}}({{.Default}}) {{color "reset"}}{{end}}
|
||||
{{- color "cyan"}}[Enter to launch editor] {{color "reset"}}
|
||||
{{- end}}`
|
||||
|
||||
var (
|
||||
bom = []byte{0xef, 0xbb, 0xbf}
|
||||
editor = "vim"
|
||||
)
|
||||
|
||||
func init() {
|
||||
if runtime.GOOS == "windows" {
|
||||
editor = "notepad"
|
||||
}
|
||||
if v := os.Getenv("VISUAL"); v != "" {
|
||||
editor = v
|
||||
} else if e := os.Getenv("EDITOR"); e != "" {
|
||||
editor = e
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Editor) PromptAgain(config *PromptConfig, invalid interface{}, err error) (interface{}, error) {
|
||||
initialValue := invalid.(string)
|
||||
return e.prompt(initialValue, config)
|
||||
}
|
||||
|
||||
func (e *Editor) Prompt(config *PromptConfig) (interface{}, error) {
|
||||
initialValue := ""
|
||||
if e.Default != "" && e.AppendDefault {
|
||||
initialValue = e.Default
|
||||
}
|
||||
return e.prompt(initialValue, config)
|
||||
}
|
||||
|
||||
func (e *Editor) prompt(initialValue string, config *PromptConfig) (interface{}, error) {
|
||||
// render the template
|
||||
err := e.Render(
|
||||
EditorQuestionTemplate,
|
||||
EditorTemplateData{
|
||||
Editor: *e,
|
||||
Config: config,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// start reading runes from the standard in
|
||||
rr := e.NewRuneReader()
|
||||
_ = rr.SetTermMode()
|
||||
defer func() {
|
||||
_ = rr.RestoreTermMode()
|
||||
}()
|
||||
|
||||
cursor := e.NewCursor()
|
||||
cursor.Hide()
|
||||
defer cursor.Show()
|
||||
|
||||
for {
|
||||
r, _, err := rr.ReadRune()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if r == '\r' || r == '\n' {
|
||||
break
|
||||
}
|
||||
if r == terminal.KeyInterrupt {
|
||||
return "", terminal.InterruptErr
|
||||
}
|
||||
if r == terminal.KeyEndTransmission {
|
||||
break
|
||||
}
|
||||
if string(r) == config.HelpInput && e.Help != "" {
|
||||
err = e.Render(
|
||||
EditorQuestionTemplate,
|
||||
EditorTemplateData{
|
||||
Editor: *e,
|
||||
ShowHelp: true,
|
||||
Config: config,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// prepare the temp file
|
||||
pattern := e.FileName
|
||||
if pattern == "" {
|
||||
pattern = "survey*.txt"
|
||||
}
|
||||
f, err := ioutil.TempFile("", pattern)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func() {
|
||||
_ = os.Remove(f.Name())
|
||||
}()
|
||||
|
||||
// write utf8 BOM header
|
||||
// The reason why we do this is because notepad.exe on Windows determines the
|
||||
// encoding of an "empty" text file by the locale, for example, GBK in China,
|
||||
// while golang string only handles utf8 well. However, a text file with utf8
|
||||
// BOM header is not considered "empty" on Windows, and the encoding will then
|
||||
// be determined utf8 by notepad.exe, instead of GBK or other encodings.
|
||||
if _, err := f.Write(bom); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// write initial value
|
||||
if _, err := f.WriteString(initialValue); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// close the fd to prevent the editor unable to save file
|
||||
if err := f.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// check is input editor exist
|
||||
if e.Editor != "" {
|
||||
editor = e.Editor
|
||||
}
|
||||
|
||||
stdio := e.Stdio()
|
||||
|
||||
args, err := shellquote.Split(editor)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
args = append(args, f.Name())
|
||||
|
||||
// open the editor
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd.Stdin = stdio.In
|
||||
cmd.Stdout = stdio.Out
|
||||
cmd.Stderr = stdio.Err
|
||||
cursor.Show()
|
||||
if err := cmd.Run(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// raw is a BOM-unstripped UTF8 byte slice
|
||||
raw, err := ioutil.ReadFile(f.Name())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// strip BOM header
|
||||
text := string(bytes.TrimPrefix(raw, bom))
|
||||
|
||||
// check length, return default value on empty
|
||||
if len(text) == 0 && !e.AppendDefault {
|
||||
return e.Default, nil
|
||||
}
|
||||
|
||||
return text, nil
|
||||
}
|
||||
|
||||
func (e *Editor) Cleanup(config *PromptConfig, val interface{}) error {
|
||||
return e.Render(
|
||||
EditorQuestionTemplate,
|
||||
EditorTemplateData{
|
||||
Editor: *e,
|
||||
Answer: "<Received>",
|
||||
ShowAnswer: true,
|
||||
Config: config,
|
||||
},
|
||||
)
|
||||
}
|
||||
-1
@@ -1 +0,0 @@
|
||||
package survey
|
||||
-219
@@ -1,219 +0,0 @@
|
||||
package survey
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2/core"
|
||||
"github.com/AlecAivazis/survey/v2/terminal"
|
||||
)
|
||||
|
||||
/*
|
||||
Input is a regular text input that prints each character the user types on the screen
|
||||
and accepts the input with the enter key. Response type is a string.
|
||||
|
||||
name := ""
|
||||
prompt := &survey.Input{ Message: "What is your name?" }
|
||||
survey.AskOne(prompt, &name)
|
||||
*/
|
||||
type Input struct {
|
||||
Renderer
|
||||
Message string
|
||||
Default string
|
||||
Help string
|
||||
Suggest func(toComplete string) []string
|
||||
answer string
|
||||
typedAnswer string
|
||||
options []core.OptionAnswer
|
||||
selectedIndex int
|
||||
showingHelp bool
|
||||
}
|
||||
|
||||
// data available to the templates when processing
|
||||
type InputTemplateData struct {
|
||||
Input
|
||||
ShowAnswer bool
|
||||
ShowHelp bool
|
||||
Answer string
|
||||
PageEntries []core.OptionAnswer
|
||||
SelectedIndex int
|
||||
Config *PromptConfig
|
||||
}
|
||||
|
||||
// Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format
|
||||
var InputQuestionTemplate = `
|
||||
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
|
||||
{{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}}
|
||||
{{- color "default+hb"}}{{ .Message }} {{color "reset"}}
|
||||
{{- if .ShowAnswer}}
|
||||
{{- color "cyan"}}{{.Answer}}{{color "reset"}}{{"\n"}}
|
||||
{{- else if .PageEntries -}}
|
||||
{{- .Answer}} [Use arrows to move, enter to select, type to continue]
|
||||
{{- "\n"}}
|
||||
{{- range $ix, $choice := .PageEntries}}
|
||||
{{- if eq $ix $.SelectedIndex }}{{color $.Config.Icons.SelectFocus.Format }}{{ $.Config.Icons.SelectFocus.Text }} {{else}}{{color "default"}} {{end}}
|
||||
{{- $choice.Value}}
|
||||
{{- color "reset"}}{{"\n"}}
|
||||
{{- end}}
|
||||
{{- else }}
|
||||
{{- if or (and .Help (not .ShowHelp)) .Suggest }}{{color "cyan"}}[
|
||||
{{- if and .Help (not .ShowHelp)}}{{ print .Config.HelpInput }} for help {{- if and .Suggest}}, {{end}}{{end -}}
|
||||
{{- if and .Suggest }}{{color "cyan"}}{{ print .Config.SuggestInput }} for suggestions{{end -}}
|
||||
]{{color "reset"}} {{end}}
|
||||
{{- if .Default}}{{color "white"}}({{.Default}}) {{color "reset"}}{{end}}
|
||||
{{- end}}`
|
||||
|
||||
func (i *Input) onRune(config *PromptConfig) terminal.OnRuneFn {
|
||||
return terminal.OnRuneFn(func(key rune, line []rune) ([]rune, bool, error) {
|
||||
if i.options != nil && (key == terminal.KeyEnter || key == '\n') {
|
||||
return []rune(i.answer), true, nil
|
||||
} else if i.options != nil && key == terminal.KeyEscape {
|
||||
i.answer = i.typedAnswer
|
||||
i.options = nil
|
||||
} else if key == terminal.KeyArrowUp && len(i.options) > 0 {
|
||||
if i.selectedIndex == 0 {
|
||||
i.selectedIndex = len(i.options) - 1
|
||||
} else {
|
||||
i.selectedIndex--
|
||||
}
|
||||
i.answer = i.options[i.selectedIndex].Value
|
||||
} else if (key == terminal.KeyArrowDown || key == terminal.KeyTab) && len(i.options) > 0 {
|
||||
if i.selectedIndex == len(i.options)-1 {
|
||||
i.selectedIndex = 0
|
||||
} else {
|
||||
i.selectedIndex++
|
||||
}
|
||||
i.answer = i.options[i.selectedIndex].Value
|
||||
} else if key == terminal.KeyTab && i.Suggest != nil {
|
||||
i.answer = string(line)
|
||||
i.typedAnswer = i.answer
|
||||
options := i.Suggest(i.answer)
|
||||
i.selectedIndex = 0
|
||||
if len(options) == 0 {
|
||||
return line, false, nil
|
||||
}
|
||||
|
||||
i.answer = options[0]
|
||||
if len(options) == 1 {
|
||||
i.typedAnswer = i.answer
|
||||
i.options = nil
|
||||
} else {
|
||||
i.options = core.OptionAnswerList(options)
|
||||
}
|
||||
} else {
|
||||
if i.options == nil {
|
||||
return line, false, nil
|
||||
}
|
||||
|
||||
if key >= terminal.KeySpace {
|
||||
i.answer += string(key)
|
||||
}
|
||||
i.typedAnswer = i.answer
|
||||
|
||||
i.options = nil
|
||||
}
|
||||
|
||||
pageSize := config.PageSize
|
||||
opts, idx := paginate(pageSize, i.options, i.selectedIndex)
|
||||
err := i.Render(
|
||||
InputQuestionTemplate,
|
||||
InputTemplateData{
|
||||
Input: *i,
|
||||
Answer: i.answer,
|
||||
ShowHelp: i.showingHelp,
|
||||
SelectedIndex: idx,
|
||||
PageEntries: opts,
|
||||
Config: config,
|
||||
},
|
||||
)
|
||||
|
||||
if err == nil {
|
||||
err = errReadLineAgain
|
||||
}
|
||||
|
||||
return []rune(i.typedAnswer), true, err
|
||||
})
|
||||
}
|
||||
|
||||
var errReadLineAgain = errors.New("read line again")
|
||||
|
||||
func (i *Input) Prompt(config *PromptConfig) (interface{}, error) {
|
||||
// render the template
|
||||
err := i.Render(
|
||||
InputQuestionTemplate,
|
||||
InputTemplateData{
|
||||
Input: *i,
|
||||
Config: config,
|
||||
ShowHelp: i.showingHelp,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// start reading runes from the standard in
|
||||
rr := i.NewRuneReader()
|
||||
_ = rr.SetTermMode()
|
||||
defer func() {
|
||||
_ = rr.RestoreTermMode()
|
||||
}()
|
||||
cursor := i.NewCursor()
|
||||
if !config.ShowCursor {
|
||||
cursor.Hide() // hide the cursor
|
||||
defer cursor.Show() // show the cursor when we're done
|
||||
}
|
||||
|
||||
var line []rune
|
||||
|
||||
for {
|
||||
if i.options != nil {
|
||||
line = []rune{}
|
||||
}
|
||||
|
||||
line, err = rr.ReadLineWithDefault(0, line, i.onRune(config))
|
||||
if err == errReadLineAgain {
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
i.answer = string(line)
|
||||
// readline print an empty line, go up before we render the follow up
|
||||
cursor.Up(1)
|
||||
|
||||
// if we ran into the help string
|
||||
if i.answer == config.HelpInput && i.Help != "" {
|
||||
// show the help and prompt again
|
||||
i.showingHelp = true
|
||||
return i.Prompt(config)
|
||||
}
|
||||
|
||||
// if the line is empty
|
||||
if len(i.answer) == 0 {
|
||||
// use the default value
|
||||
return i.Default, err
|
||||
}
|
||||
|
||||
lineStr := i.answer
|
||||
|
||||
i.AppendRenderedText(lineStr)
|
||||
|
||||
// we're done
|
||||
return lineStr, err
|
||||
}
|
||||
|
||||
func (i *Input) Cleanup(config *PromptConfig, val interface{}) error {
|
||||
return i.Render(
|
||||
InputQuestionTemplate,
|
||||
InputTemplateData{
|
||||
Input: *i,
|
||||
ShowAnswer: true,
|
||||
Config: config,
|
||||
Answer: val.(string),
|
||||
},
|
||||
)
|
||||
}
|
||||
-112
@@ -1,112 +0,0 @@
|
||||
package survey
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2/terminal"
|
||||
)
|
||||
|
||||
type Multiline struct {
|
||||
Renderer
|
||||
Message string
|
||||
Default string
|
||||
Help string
|
||||
}
|
||||
|
||||
// data available to the templates when processing
|
||||
type MultilineTemplateData struct {
|
||||
Multiline
|
||||
Answer string
|
||||
ShowAnswer bool
|
||||
ShowHelp bool
|
||||
Config *PromptConfig
|
||||
}
|
||||
|
||||
// Templates with Color formatting. See Documentation: https://github.com/mgutz/ansi#style-format
|
||||
var MultilineQuestionTemplate = `
|
||||
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
|
||||
{{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}}
|
||||
{{- color "default+hb"}}{{ .Message }} {{color "reset"}}
|
||||
{{- if .ShowAnswer}}
|
||||
{{- "\n"}}{{color "cyan"}}{{.Answer}}{{color "reset"}}
|
||||
{{- if .Answer }}{{ "\n" }}{{ end }}
|
||||
{{- else }}
|
||||
{{- if .Default}}{{color "white"}}({{.Default}}) {{color "reset"}}{{end}}
|
||||
{{- color "cyan"}}[Enter 2 empty lines to finish]{{color "reset"}}
|
||||
{{- end}}`
|
||||
|
||||
func (i *Multiline) Prompt(config *PromptConfig) (interface{}, error) {
|
||||
// render the template
|
||||
err := i.Render(
|
||||
MultilineQuestionTemplate,
|
||||
MultilineTemplateData{
|
||||
Multiline: *i,
|
||||
Config: config,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// start reading runes from the standard in
|
||||
rr := i.NewRuneReader()
|
||||
_ = rr.SetTermMode()
|
||||
defer func() {
|
||||
_ = rr.RestoreTermMode()
|
||||
}()
|
||||
|
||||
cursor := i.NewCursor()
|
||||
|
||||
multiline := make([]string, 0)
|
||||
|
||||
emptyOnce := false
|
||||
// get the next line
|
||||
for {
|
||||
var line []rune
|
||||
line, err = rr.ReadLine(0)
|
||||
if err != nil {
|
||||
return string(line), err
|
||||
}
|
||||
|
||||
if string(line) == "" {
|
||||
if emptyOnce {
|
||||
numLines := len(multiline) + 2
|
||||
cursor.PreviousLine(numLines)
|
||||
for j := 0; j < numLines; j++ {
|
||||
terminal.EraseLine(i.Stdio().Out, terminal.ERASE_LINE_ALL)
|
||||
cursor.NextLine(1)
|
||||
}
|
||||
cursor.PreviousLine(numLines)
|
||||
break
|
||||
}
|
||||
emptyOnce = true
|
||||
} else {
|
||||
emptyOnce = false
|
||||
}
|
||||
multiline = append(multiline, string(line))
|
||||
}
|
||||
|
||||
val := strings.Join(multiline, "\n")
|
||||
val = strings.TrimSpace(val)
|
||||
|
||||
// if the line is empty
|
||||
if len(val) == 0 {
|
||||
// use the default value
|
||||
return i.Default, err
|
||||
}
|
||||
|
||||
i.AppendRenderedText(val)
|
||||
return val, err
|
||||
}
|
||||
|
||||
func (i *Multiline) Cleanup(config *PromptConfig, val interface{}) error {
|
||||
return i.Render(
|
||||
MultilineQuestionTemplate,
|
||||
MultilineTemplateData{
|
||||
Multiline: *i,
|
||||
Answer: val.(string),
|
||||
ShowAnswer: true,
|
||||
Config: config,
|
||||
},
|
||||
)
|
||||
}
|
||||
-360
@@ -1,360 +0,0 @@
|
||||
package survey
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2/core"
|
||||
"github.com/AlecAivazis/survey/v2/terminal"
|
||||
)
|
||||
|
||||
/*
|
||||
MultiSelect is a prompt that presents a list of various options to the user
|
||||
for them to select using the arrow keys and enter. Response type is a slice of strings.
|
||||
|
||||
days := []string{}
|
||||
prompt := &survey.MultiSelect{
|
||||
Message: "What days do you prefer:",
|
||||
Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"},
|
||||
}
|
||||
survey.AskOne(prompt, &days)
|
||||
*/
|
||||
type MultiSelect struct {
|
||||
Renderer
|
||||
Message string
|
||||
Options []string
|
||||
Default interface{}
|
||||
Help string
|
||||
PageSize int
|
||||
VimMode bool
|
||||
FilterMessage string
|
||||
Filter func(filter string, value string, index int) bool
|
||||
Description func(value string, index int) string
|
||||
filter string
|
||||
selectedIndex int
|
||||
checked map[int]bool
|
||||
showingHelp bool
|
||||
}
|
||||
|
||||
// data available to the templates when processing
|
||||
type MultiSelectTemplateData struct {
|
||||
MultiSelect
|
||||
Answer string
|
||||
ShowAnswer bool
|
||||
Checked map[int]bool
|
||||
SelectedIndex int
|
||||
ShowHelp bool
|
||||
Description func(value string, index int) string
|
||||
PageEntries []core.OptionAnswer
|
||||
Config *PromptConfig
|
||||
|
||||
// These fields are used when rendering an individual option
|
||||
CurrentOpt core.OptionAnswer
|
||||
CurrentIndex int
|
||||
}
|
||||
|
||||
// IterateOption sets CurrentOpt and CurrentIndex appropriately so a multiselect option can be rendered individually
|
||||
func (m MultiSelectTemplateData) IterateOption(ix int, opt core.OptionAnswer) interface{} {
|
||||
copy := m
|
||||
copy.CurrentIndex = ix
|
||||
copy.CurrentOpt = opt
|
||||
return copy
|
||||
}
|
||||
|
||||
func (m MultiSelectTemplateData) GetDescription(opt core.OptionAnswer) string {
|
||||
if m.Description == nil {
|
||||
return ""
|
||||
}
|
||||
return m.Description(opt.Value, opt.Index)
|
||||
}
|
||||
|
||||
var MultiSelectQuestionTemplate = `
|
||||
{{- define "option"}}
|
||||
{{- if eq .SelectedIndex .CurrentIndex }}{{color .Config.Icons.SelectFocus.Format }}{{ .Config.Icons.SelectFocus.Text }}{{color "reset"}}{{else}} {{end}}
|
||||
{{- if index .Checked .CurrentOpt.Index }}{{color .Config.Icons.MarkedOption.Format }} {{ .Config.Icons.MarkedOption.Text }} {{else}}{{color .Config.Icons.UnmarkedOption.Format }} {{ .Config.Icons.UnmarkedOption.Text }} {{end}}
|
||||
{{- color "reset"}}
|
||||
{{- " "}}{{- .CurrentOpt.Value}}{{ if ne ($.GetDescription .CurrentOpt) "" }} - {{color "cyan"}}{{ $.GetDescription .CurrentOpt }}{{color "reset"}}{{end}}
|
||||
{{end}}
|
||||
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
|
||||
{{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}}
|
||||
{{- color "default+hb"}}{{ .Message }}{{ .FilterMessage }}{{color "reset"}}
|
||||
{{- if .ShowAnswer}}{{color "cyan"}} {{.Answer}}{{color "reset"}}{{"\n"}}
|
||||
{{- else }}
|
||||
{{- " "}}{{- color "cyan"}}[Use arrows to move, space to select,{{- if not .Config.RemoveSelectAll }} <right> to all,{{end}}{{- if not .Config.RemoveSelectNone }} <left> to none,{{end}} type to filter{{- if and .Help (not .ShowHelp)}}, {{ .Config.HelpInput }} for more help{{end}}]{{color "reset"}}
|
||||
{{- "\n"}}
|
||||
{{- range $ix, $option := .PageEntries}}
|
||||
{{- template "option" $.IterateOption $ix $option}}
|
||||
{{- end}}
|
||||
{{- end}}`
|
||||
|
||||
// OnChange is called on every keypress.
|
||||
func (m *MultiSelect) OnChange(key rune, config *PromptConfig) {
|
||||
options := m.filterOptions(config)
|
||||
oldFilter := m.filter
|
||||
|
||||
if key == terminal.KeyArrowUp || (m.VimMode && key == 'k') {
|
||||
// if we are at the top of the list
|
||||
if m.selectedIndex == 0 {
|
||||
// go to the bottom
|
||||
m.selectedIndex = len(options) - 1
|
||||
} else {
|
||||
// decrement the selected index
|
||||
m.selectedIndex--
|
||||
}
|
||||
} else if key == terminal.KeyTab || key == terminal.KeyArrowDown || (m.VimMode && key == 'j') {
|
||||
// if we are at the bottom of the list
|
||||
if m.selectedIndex == len(options)-1 {
|
||||
// start at the top
|
||||
m.selectedIndex = 0
|
||||
} else {
|
||||
// increment the selected index
|
||||
m.selectedIndex++
|
||||
}
|
||||
// if the user pressed down and there is room to move
|
||||
} else if key == terminal.KeySpace {
|
||||
// the option they have selected
|
||||
if m.selectedIndex < len(options) {
|
||||
selectedOpt := options[m.selectedIndex]
|
||||
|
||||
// if we haven't seen this index before
|
||||
if old, ok := m.checked[selectedOpt.Index]; !ok {
|
||||
// set the value to true
|
||||
m.checked[selectedOpt.Index] = true
|
||||
} else {
|
||||
// otherwise just invert the current value
|
||||
m.checked[selectedOpt.Index] = !old
|
||||
}
|
||||
if !config.KeepFilter {
|
||||
m.filter = ""
|
||||
}
|
||||
}
|
||||
// only show the help message if we have one to show
|
||||
} else if string(key) == config.HelpInput && m.Help != "" {
|
||||
m.showingHelp = true
|
||||
} else if key == terminal.KeyEscape {
|
||||
m.VimMode = !m.VimMode
|
||||
} else if key == terminal.KeyDeleteWord || key == terminal.KeyDeleteLine {
|
||||
m.filter = ""
|
||||
} else if key == terminal.KeyDelete || key == terminal.KeyBackspace {
|
||||
if m.filter != "" {
|
||||
runeFilter := []rune(m.filter)
|
||||
m.filter = string(runeFilter[0 : len(runeFilter)-1])
|
||||
}
|
||||
} else if key >= terminal.KeySpace {
|
||||
m.filter += string(key)
|
||||
m.VimMode = false
|
||||
} else if !config.RemoveSelectAll && key == terminal.KeyArrowRight {
|
||||
for _, v := range options {
|
||||
m.checked[v.Index] = true
|
||||
}
|
||||
if !config.KeepFilter {
|
||||
m.filter = ""
|
||||
}
|
||||
} else if !config.RemoveSelectNone && key == terminal.KeyArrowLeft {
|
||||
for _, v := range options {
|
||||
m.checked[v.Index] = false
|
||||
}
|
||||
if !config.KeepFilter {
|
||||
m.filter = ""
|
||||
}
|
||||
}
|
||||
|
||||
m.FilterMessage = ""
|
||||
if m.filter != "" {
|
||||
m.FilterMessage = " " + m.filter
|
||||
}
|
||||
if oldFilter != m.filter {
|
||||
// filter changed
|
||||
options = m.filterOptions(config)
|
||||
if len(options) > 0 && len(options) <= m.selectedIndex {
|
||||
m.selectedIndex = len(options) - 1
|
||||
}
|
||||
}
|
||||
// paginate the options
|
||||
// figure out the page size
|
||||
pageSize := m.PageSize
|
||||
// if we dont have a specific one
|
||||
if pageSize == 0 {
|
||||
// grab the global value
|
||||
pageSize = config.PageSize
|
||||
}
|
||||
|
||||
// TODO if we have started filtering and were looking at the end of a list
|
||||
// and we have modified the filter then we should move the page back!
|
||||
opts, idx := paginate(pageSize, options, m.selectedIndex)
|
||||
|
||||
tmplData := MultiSelectTemplateData{
|
||||
MultiSelect: *m,
|
||||
SelectedIndex: idx,
|
||||
Checked: m.checked,
|
||||
ShowHelp: m.showingHelp,
|
||||
Description: m.Description,
|
||||
PageEntries: opts,
|
||||
Config: config,
|
||||
}
|
||||
|
||||
// render the options
|
||||
_ = m.RenderWithCursorOffset(MultiSelectQuestionTemplate, tmplData, opts, idx)
|
||||
}
|
||||
|
||||
func (m *MultiSelect) filterOptions(config *PromptConfig) []core.OptionAnswer {
|
||||
// the filtered list
|
||||
answers := []core.OptionAnswer{}
|
||||
|
||||
// if there is no filter applied
|
||||
if m.filter == "" {
|
||||
// return all of the options
|
||||
return core.OptionAnswerList(m.Options)
|
||||
}
|
||||
|
||||
// the filter to apply
|
||||
filter := m.Filter
|
||||
if filter == nil {
|
||||
filter = config.Filter
|
||||
}
|
||||
|
||||
// apply the filter to each option
|
||||
for i, opt := range m.Options {
|
||||
// i the filter says to include the option
|
||||
if filter(m.filter, opt, i) {
|
||||
answers = append(answers, core.OptionAnswer{
|
||||
Index: i,
|
||||
Value: opt,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// we're done here
|
||||
return answers
|
||||
}
|
||||
|
||||
func (m *MultiSelect) Prompt(config *PromptConfig) (interface{}, error) {
|
||||
// compute the default state
|
||||
m.checked = make(map[int]bool)
|
||||
// if there is a default
|
||||
if m.Default != nil {
|
||||
// if the default is string values
|
||||
if defaultValues, ok := m.Default.([]string); ok {
|
||||
for _, dflt := range defaultValues {
|
||||
for i, opt := range m.Options {
|
||||
// if the option corresponds to the default
|
||||
if opt == dflt {
|
||||
// we found our initial value
|
||||
m.checked[i] = true
|
||||
// stop looking
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// if the default value is index values
|
||||
} else if defaultIndices, ok := m.Default.([]int); ok {
|
||||
// go over every index we need to enable by default
|
||||
for _, idx := range defaultIndices {
|
||||
// and enable it
|
||||
m.checked[idx] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if there are no options to render
|
||||
if len(m.Options) == 0 {
|
||||
// we failed
|
||||
return "", errors.New("please provide options to select from")
|
||||
}
|
||||
|
||||
// figure out the page size
|
||||
pageSize := m.PageSize
|
||||
// if we dont have a specific one
|
||||
if pageSize == 0 {
|
||||
// grab the global value
|
||||
pageSize = config.PageSize
|
||||
}
|
||||
// paginate the options
|
||||
// build up a list of option answers
|
||||
opts, idx := paginate(pageSize, core.OptionAnswerList(m.Options), m.selectedIndex)
|
||||
|
||||
cursor := m.NewCursor()
|
||||
cursor.Save() // for proper cursor placement during selection
|
||||
cursor.Hide() // hide the cursor
|
||||
defer cursor.Show() // show the cursor when we're done
|
||||
defer cursor.Restore() // clear any accessibility offsetting on exit
|
||||
|
||||
tmplData := MultiSelectTemplateData{
|
||||
MultiSelect: *m,
|
||||
SelectedIndex: idx,
|
||||
Description: m.Description,
|
||||
Checked: m.checked,
|
||||
PageEntries: opts,
|
||||
Config: config,
|
||||
}
|
||||
|
||||
// ask the question
|
||||
err := m.RenderWithCursorOffset(MultiSelectQuestionTemplate, tmplData, opts, idx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
rr := m.NewRuneReader()
|
||||
_ = rr.SetTermMode()
|
||||
defer func() {
|
||||
_ = rr.RestoreTermMode()
|
||||
}()
|
||||
|
||||
// start waiting for input
|
||||
for {
|
||||
r, _, err := rr.ReadRune()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if r == '\r' || r == '\n' {
|
||||
break
|
||||
}
|
||||
if r == terminal.KeyInterrupt {
|
||||
return "", terminal.InterruptErr
|
||||
}
|
||||
if r == terminal.KeyEndTransmission {
|
||||
break
|
||||
}
|
||||
m.OnChange(r, config)
|
||||
}
|
||||
m.filter = ""
|
||||
m.FilterMessage = ""
|
||||
|
||||
answers := []core.OptionAnswer{}
|
||||
for i, option := range m.Options {
|
||||
if val, ok := m.checked[i]; ok && val {
|
||||
answers = append(answers, core.OptionAnswer{Value: option, Index: i})
|
||||
}
|
||||
}
|
||||
|
||||
return answers, nil
|
||||
}
|
||||
|
||||
// Cleanup removes the options section, and renders the ask like a normal question.
|
||||
func (m *MultiSelect) Cleanup(config *PromptConfig, val interface{}) error {
|
||||
// the answer to show
|
||||
answer := ""
|
||||
for _, ans := range val.([]core.OptionAnswer) {
|
||||
answer = fmt.Sprintf("%s, %s", answer, ans.Value)
|
||||
}
|
||||
|
||||
// if we answered anything
|
||||
if len(answer) > 2 {
|
||||
// remove the precending commas
|
||||
answer = answer[2:]
|
||||
}
|
||||
|
||||
// execute the output summary template with the answer
|
||||
return m.Render(
|
||||
MultiSelectQuestionTemplate,
|
||||
MultiSelectTemplateData{
|
||||
MultiSelect: *m,
|
||||
SelectedIndex: m.selectedIndex,
|
||||
Checked: m.checked,
|
||||
Answer: answer,
|
||||
ShowAnswer: true,
|
||||
Description: m.Description,
|
||||
Config: config,
|
||||
},
|
||||
)
|
||||
}
|
||||
-106
@@ -1,106 +0,0 @@
|
||||
package survey
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2/core"
|
||||
"github.com/AlecAivazis/survey/v2/terminal"
|
||||
)
|
||||
|
||||
/*
|
||||
Password is like a normal Input but the text shows up as *'s and there is no default. Response
|
||||
type is a string.
|
||||
|
||||
password := ""
|
||||
prompt := &survey.Password{ Message: "Please type your password" }
|
||||
survey.AskOne(prompt, &password)
|
||||
*/
|
||||
type Password struct {
|
||||
Renderer
|
||||
Message string
|
||||
Help string
|
||||
}
|
||||
|
||||
type PasswordTemplateData struct {
|
||||
Password
|
||||
ShowHelp bool
|
||||
Config *PromptConfig
|
||||
}
|
||||
|
||||
// PasswordQuestionTemplate is a template with color formatting. See Documentation: https://github.com/mgutz/ansi#style-format
|
||||
var PasswordQuestionTemplate = `
|
||||
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
|
||||
{{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}}
|
||||
{{- color "default+hb"}}{{ .Message }} {{color "reset"}}
|
||||
{{- if and .Help (not .ShowHelp)}}{{color "cyan"}}[{{ .Config.HelpInput }} for help]{{color "reset"}} {{end}}`
|
||||
|
||||
func (p *Password) Prompt(config *PromptConfig) (interface{}, error) {
|
||||
// render the question template
|
||||
userOut, _, err := core.RunTemplate(
|
||||
PasswordQuestionTemplate,
|
||||
PasswordTemplateData{
|
||||
Password: *p,
|
||||
Config: config,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprint(terminal.NewAnsiStdout(p.Stdio().Out), userOut); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
rr := p.NewRuneReader()
|
||||
_ = rr.SetTermMode()
|
||||
defer func() {
|
||||
_ = rr.RestoreTermMode()
|
||||
}()
|
||||
|
||||
// no help msg? Just return any response
|
||||
if p.Help == "" {
|
||||
line, err := rr.ReadLine(config.HideCharacter)
|
||||
return string(line), err
|
||||
}
|
||||
|
||||
cursor := p.NewCursor()
|
||||
|
||||
var line []rune
|
||||
// process answers looking for help prompt answer
|
||||
for {
|
||||
line, err = rr.ReadLine(config.HideCharacter)
|
||||
if err != nil {
|
||||
return string(line), err
|
||||
}
|
||||
|
||||
if string(line) == config.HelpInput {
|
||||
// terminal will echo the \n so we need to jump back up one row
|
||||
cursor.PreviousLine(1)
|
||||
|
||||
err = p.Render(
|
||||
PasswordQuestionTemplate,
|
||||
PasswordTemplateData{
|
||||
Password: *p,
|
||||
ShowHelp: true,
|
||||
Config: config,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
lineStr := string(line)
|
||||
p.AppendRenderedText(strings.Repeat(string(config.HideCharacter), len(lineStr)))
|
||||
return lineStr, err
|
||||
}
|
||||
|
||||
// Cleanup hides the string with a fixed number of characters.
|
||||
func (prompt *Password) Cleanup(config *PromptConfig, val interface{}) error {
|
||||
return nil
|
||||
}
|
||||
-195
@@ -1,195 +0,0 @@
|
||||
package survey
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/AlecAivazis/survey/v2/core"
|
||||
"github.com/AlecAivazis/survey/v2/terminal"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
type Renderer struct {
|
||||
stdio terminal.Stdio
|
||||
renderedErrors bytes.Buffer
|
||||
renderedText bytes.Buffer
|
||||
}
|
||||
|
||||
type ErrorTemplateData struct {
|
||||
Error error
|
||||
Icon Icon
|
||||
}
|
||||
|
||||
var ErrorTemplate = `{{color .Icon.Format }}{{ .Icon.Text }} Sorry, your reply was invalid: {{ .Error.Error }}{{color "reset"}}
|
||||
`
|
||||
|
||||
func (r *Renderer) WithStdio(stdio terminal.Stdio) {
|
||||
r.stdio = stdio
|
||||
}
|
||||
|
||||
func (r *Renderer) Stdio() terminal.Stdio {
|
||||
return r.stdio
|
||||
}
|
||||
|
||||
func (r *Renderer) NewRuneReader() *terminal.RuneReader {
|
||||
return terminal.NewRuneReader(r.stdio)
|
||||
}
|
||||
|
||||
func (r *Renderer) NewCursor() *terminal.Cursor {
|
||||
return &terminal.Cursor{
|
||||
In: r.stdio.In,
|
||||
Out: r.stdio.Out,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Renderer) Error(config *PromptConfig, invalid error) error {
|
||||
// cleanup the currently rendered errors
|
||||
r.resetPrompt(r.countLines(r.renderedErrors))
|
||||
r.renderedErrors.Reset()
|
||||
|
||||
// cleanup the rest of the prompt
|
||||
r.resetPrompt(r.countLines(r.renderedText))
|
||||
r.renderedText.Reset()
|
||||
|
||||
userOut, layoutOut, err := core.RunTemplate(ErrorTemplate, &ErrorTemplateData{
|
||||
Error: invalid,
|
||||
Icon: config.Icons.Error,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// send the message to the user
|
||||
if _, err := fmt.Fprint(terminal.NewAnsiStdout(r.stdio.Out), userOut); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add the printed text to the rendered error buffer so we can cleanup later
|
||||
r.appendRenderedError(layoutOut)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Renderer) OffsetCursor(offset int) {
|
||||
cursor := r.NewCursor()
|
||||
for offset > 0 {
|
||||
cursor.PreviousLine(1)
|
||||
offset--
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Renderer) Render(tmpl string, data interface{}) error {
|
||||
// cleanup the currently rendered text
|
||||
lineCount := r.countLines(r.renderedText)
|
||||
r.resetPrompt(lineCount)
|
||||
r.renderedText.Reset()
|
||||
|
||||
// render the template summarizing the current state
|
||||
userOut, layoutOut, err := core.RunTemplate(tmpl, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// print the summary
|
||||
if _, err := fmt.Fprint(terminal.NewAnsiStdout(r.stdio.Out), userOut); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add the printed text to the rendered text buffer so we can cleanup later
|
||||
r.AppendRenderedText(layoutOut)
|
||||
|
||||
// nothing went wrong
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Renderer) RenderWithCursorOffset(tmpl string, data IterableOpts, opts []core.OptionAnswer, idx int) error {
|
||||
cursor := r.NewCursor()
|
||||
cursor.Restore() // clear any accessibility offsetting
|
||||
|
||||
if err := r.Render(tmpl, data); err != nil {
|
||||
return err
|
||||
}
|
||||
cursor.Save()
|
||||
|
||||
offset := computeCursorOffset(MultiSelectQuestionTemplate, data, opts, idx, r.termWidthSafe())
|
||||
r.OffsetCursor(offset)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// appendRenderedError appends text to the renderer's error buffer
|
||||
// which is used to track what has been printed. It is not exported
|
||||
// as errors should only be displayed via Error(config, error).
|
||||
func (r *Renderer) appendRenderedError(text string) {
|
||||
r.renderedErrors.WriteString(text)
|
||||
}
|
||||
|
||||
// AppendRenderedText appends text to the renderer's text buffer
|
||||
// which is used to track of what has been printed. The buffer is used
|
||||
// to calculate how many lines to erase before updating the prompt.
|
||||
func (r *Renderer) AppendRenderedText(text string) {
|
||||
r.renderedText.WriteString(text)
|
||||
}
|
||||
|
||||
func (r *Renderer) resetPrompt(lines int) {
|
||||
// clean out current line in case tmpl didnt end in newline
|
||||
cursor := r.NewCursor()
|
||||
cursor.HorizontalAbsolute(0)
|
||||
terminal.EraseLine(r.stdio.Out, terminal.ERASE_LINE_ALL)
|
||||
// clean up what we left behind last time
|
||||
for i := 0; i < lines; i++ {
|
||||
cursor.PreviousLine(1)
|
||||
terminal.EraseLine(r.stdio.Out, terminal.ERASE_LINE_ALL)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Renderer) termWidth() (int, error) {
|
||||
fd := int(r.stdio.Out.Fd())
|
||||
termWidth, _, err := term.GetSize(fd)
|
||||
return termWidth, err
|
||||
}
|
||||
|
||||
func (r *Renderer) termWidthSafe() int {
|
||||
w, err := r.termWidth()
|
||||
if err != nil || w == 0 {
|
||||
// if we got an error due to terminal.GetSize not being supported
|
||||
// on current platform then just assume a very wide terminal
|
||||
w = 10000
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
// countLines will return the count of `\n` with the addition of any
|
||||
// lines that have wrapped due to narrow terminal width
|
||||
func (r *Renderer) countLines(buf bytes.Buffer) int {
|
||||
w := r.termWidthSafe()
|
||||
|
||||
bufBytes := buf.Bytes()
|
||||
|
||||
count := 0
|
||||
curr := 0
|
||||
for curr < len(bufBytes) {
|
||||
var delim int
|
||||
// read until the next newline or the end of the string
|
||||
relDelim := bytes.IndexRune(bufBytes[curr:], '\n')
|
||||
if relDelim != -1 {
|
||||
count += 1 // new line found, add it to the count
|
||||
delim = curr + relDelim
|
||||
} else {
|
||||
delim = len(bufBytes) // no new line found, read rest of text
|
||||
}
|
||||
|
||||
str := string(bufBytes[curr:delim])
|
||||
if lineWidth := terminal.StringWidth(str); lineWidth > w {
|
||||
// account for word wrapping
|
||||
count += lineWidth / w
|
||||
if (lineWidth % w) == 0 {
|
||||
// content whose width is exactly a multiplier of available width should not
|
||||
// count as having wrapped on the last line
|
||||
count -= 1
|
||||
}
|
||||
}
|
||||
curr = delim + 1
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
-329
@@ -1,329 +0,0 @@
|
||||
package survey
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2/core"
|
||||
"github.com/AlecAivazis/survey/v2/terminal"
|
||||
)
|
||||
|
||||
/*
|
||||
Select is a prompt that presents a list of various options to the user
|
||||
for them to select using the arrow keys and enter. Response type is a string.
|
||||
|
||||
color := ""
|
||||
prompt := &survey.Select{
|
||||
Message: "Choose a color:",
|
||||
Options: []string{"red", "blue", "green"},
|
||||
}
|
||||
survey.AskOne(prompt, &color)
|
||||
*/
|
||||
type Select struct {
|
||||
Renderer
|
||||
Message string
|
||||
Options []string
|
||||
Default interface{}
|
||||
Help string
|
||||
PageSize int
|
||||
VimMode bool
|
||||
FilterMessage string
|
||||
Filter func(filter string, value string, index int) bool
|
||||
Description func(value string, index int) string
|
||||
filter string
|
||||
selectedIndex int
|
||||
showingHelp bool
|
||||
}
|
||||
|
||||
// SelectTemplateData is the data available to the templates when processing
|
||||
type SelectTemplateData struct {
|
||||
Select
|
||||
PageEntries []core.OptionAnswer
|
||||
SelectedIndex int
|
||||
Answer string
|
||||
ShowAnswer bool
|
||||
ShowHelp bool
|
||||
Description func(value string, index int) string
|
||||
Config *PromptConfig
|
||||
|
||||
// These fields are used when rendering an individual option
|
||||
CurrentOpt core.OptionAnswer
|
||||
CurrentIndex int
|
||||
}
|
||||
|
||||
// IterateOption sets CurrentOpt and CurrentIndex appropriately so a select option can be rendered individually
|
||||
func (s SelectTemplateData) IterateOption(ix int, opt core.OptionAnswer) interface{} {
|
||||
copy := s
|
||||
copy.CurrentIndex = ix
|
||||
copy.CurrentOpt = opt
|
||||
return copy
|
||||
}
|
||||
|
||||
func (s SelectTemplateData) GetDescription(opt core.OptionAnswer) string {
|
||||
if s.Description == nil {
|
||||
return ""
|
||||
}
|
||||
return s.Description(opt.Value, opt.Index)
|
||||
}
|
||||
|
||||
var SelectQuestionTemplate = `
|
||||
{{- define "option"}}
|
||||
{{- if eq .SelectedIndex .CurrentIndex }}{{color .Config.Icons.SelectFocus.Format }}{{ .Config.Icons.SelectFocus.Text }} {{else}}{{color "default"}} {{end}}
|
||||
{{- .CurrentOpt.Value}}{{ if ne ($.GetDescription .CurrentOpt) "" }} - {{color "cyan"}}{{ $.GetDescription .CurrentOpt }}{{end}}
|
||||
{{- color "reset"}}
|
||||
{{end}}
|
||||
{{- if .ShowHelp }}{{- color .Config.Icons.Help.Format }}{{ .Config.Icons.Help.Text }} {{ .Help }}{{color "reset"}}{{"\n"}}{{end}}
|
||||
{{- color .Config.Icons.Question.Format }}{{ .Config.Icons.Question.Text }} {{color "reset"}}
|
||||
{{- color "default+hb"}}{{ .Message }}{{ .FilterMessage }}{{color "reset"}}
|
||||
{{- if .ShowAnswer}}{{color "cyan"}} {{.Answer}}{{color "reset"}}{{"\n"}}
|
||||
{{- else}}
|
||||
{{- " "}}{{- color "cyan"}}[Use arrows to move, type to filter{{- if and .Help (not .ShowHelp)}}, {{ .Config.HelpInput }} for more help{{end}}]{{color "reset"}}
|
||||
{{- "\n"}}
|
||||
{{- range $ix, $option := .PageEntries}}
|
||||
{{- template "option" $.IterateOption $ix $option}}
|
||||
{{- end}}
|
||||
{{- end}}`
|
||||
|
||||
// OnChange is called on every keypress.
|
||||
func (s *Select) OnChange(key rune, config *PromptConfig) bool {
|
||||
options := s.filterOptions(config)
|
||||
oldFilter := s.filter
|
||||
|
||||
// if the user pressed the enter key and the index is a valid option
|
||||
if key == terminal.KeyEnter || key == '\n' {
|
||||
// if the selected index is a valid option
|
||||
if len(options) > 0 && s.selectedIndex < len(options) {
|
||||
|
||||
// we're done (stop prompting the user)
|
||||
return true
|
||||
}
|
||||
|
||||
// we're not done (keep prompting)
|
||||
return false
|
||||
|
||||
// if the user pressed the up arrow or 'k' to emulate vim
|
||||
} else if (key == terminal.KeyArrowUp || (s.VimMode && key == 'k')) && len(options) > 0 {
|
||||
// if we are at the top of the list
|
||||
if s.selectedIndex == 0 {
|
||||
// start from the button
|
||||
s.selectedIndex = len(options) - 1
|
||||
} else {
|
||||
// otherwise we are not at the top of the list so decrement the selected index
|
||||
s.selectedIndex--
|
||||
}
|
||||
|
||||
// if the user pressed down or 'j' to emulate vim
|
||||
} else if (key == terminal.KeyTab || key == terminal.KeyArrowDown || (s.VimMode && key == 'j')) && len(options) > 0 {
|
||||
// if we are at the bottom of the list
|
||||
if s.selectedIndex == len(options)-1 {
|
||||
// start from the top
|
||||
s.selectedIndex = 0
|
||||
} else {
|
||||
// increment the selected index
|
||||
s.selectedIndex++
|
||||
}
|
||||
// only show the help message if we have one
|
||||
} else if string(key) == config.HelpInput && s.Help != "" {
|
||||
s.showingHelp = true
|
||||
// if the user wants to toggle vim mode on/off
|
||||
} else if key == terminal.KeyEscape {
|
||||
s.VimMode = !s.VimMode
|
||||
// if the user hits any of the keys that clear the filter
|
||||
} else if key == terminal.KeyDeleteWord || key == terminal.KeyDeleteLine {
|
||||
s.filter = ""
|
||||
// if the user is deleting a character in the filter
|
||||
} else if key == terminal.KeyDelete || key == terminal.KeyBackspace {
|
||||
// if there is content in the filter to delete
|
||||
if s.filter != "" {
|
||||
runeFilter := []rune(s.filter)
|
||||
// subtract a line from the current filter
|
||||
s.filter = string(runeFilter[0 : len(runeFilter)-1])
|
||||
// we removed the last value in the filter
|
||||
}
|
||||
} else if key >= terminal.KeySpace {
|
||||
s.filter += string(key)
|
||||
// make sure vim mode is disabled
|
||||
s.VimMode = false
|
||||
}
|
||||
|
||||
s.FilterMessage = ""
|
||||
if s.filter != "" {
|
||||
s.FilterMessage = " " + s.filter
|
||||
}
|
||||
if oldFilter != s.filter {
|
||||
// filter changed
|
||||
options = s.filterOptions(config)
|
||||
if len(options) > 0 && len(options) <= s.selectedIndex {
|
||||
s.selectedIndex = len(options) - 1
|
||||
}
|
||||
}
|
||||
|
||||
// figure out the options and index to render
|
||||
// figure out the page size
|
||||
pageSize := s.PageSize
|
||||
// if we dont have a specific one
|
||||
if pageSize == 0 {
|
||||
// grab the global value
|
||||
pageSize = config.PageSize
|
||||
}
|
||||
|
||||
// TODO if we have started filtering and were looking at the end of a list
|
||||
// and we have modified the filter then we should move the page back!
|
||||
opts, idx := paginate(pageSize, options, s.selectedIndex)
|
||||
|
||||
tmplData := SelectTemplateData{
|
||||
Select: *s,
|
||||
SelectedIndex: idx,
|
||||
ShowHelp: s.showingHelp,
|
||||
Description: s.Description,
|
||||
PageEntries: opts,
|
||||
Config: config,
|
||||
}
|
||||
|
||||
// render the options
|
||||
_ = s.RenderWithCursorOffset(SelectQuestionTemplate, tmplData, opts, idx)
|
||||
|
||||
// keep prompting
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Select) filterOptions(config *PromptConfig) []core.OptionAnswer {
|
||||
// the filtered list
|
||||
answers := []core.OptionAnswer{}
|
||||
|
||||
// if there is no filter applied
|
||||
if s.filter == "" {
|
||||
return core.OptionAnswerList(s.Options)
|
||||
}
|
||||
|
||||
// the filter to apply
|
||||
filter := s.Filter
|
||||
if filter == nil {
|
||||
filter = config.Filter
|
||||
}
|
||||
|
||||
for i, opt := range s.Options {
|
||||
// i the filter says to include the option
|
||||
if filter(s.filter, opt, i) {
|
||||
answers = append(answers, core.OptionAnswer{
|
||||
Index: i,
|
||||
Value: opt,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// return the list of answers
|
||||
return answers
|
||||
}
|
||||
|
||||
func (s *Select) Prompt(config *PromptConfig) (interface{}, error) {
|
||||
// if there are no options to render
|
||||
if len(s.Options) == 0 {
|
||||
// we failed
|
||||
return "", errors.New("please provide options to select from")
|
||||
}
|
||||
|
||||
s.selectedIndex = 0
|
||||
if s.Default != nil {
|
||||
switch defaultValue := s.Default.(type) {
|
||||
case string:
|
||||
var found bool
|
||||
for i, opt := range s.Options {
|
||||
if opt == defaultValue {
|
||||
s.selectedIndex = i
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return "", fmt.Errorf("default value %q not found in options", defaultValue)
|
||||
}
|
||||
case int:
|
||||
if defaultValue >= len(s.Options) {
|
||||
return "", fmt.Errorf("default index %d exceeds the number of options", defaultValue)
|
||||
}
|
||||
s.selectedIndex = defaultValue
|
||||
default:
|
||||
return "", errors.New("default value of select must be an int or string")
|
||||
}
|
||||
}
|
||||
|
||||
// figure out the page size
|
||||
pageSize := s.PageSize
|
||||
// if we dont have a specific one
|
||||
if pageSize == 0 {
|
||||
// grab the global value
|
||||
pageSize = config.PageSize
|
||||
}
|
||||
|
||||
// figure out the options and index to render
|
||||
opts, idx := paginate(pageSize, core.OptionAnswerList(s.Options), s.selectedIndex)
|
||||
|
||||
cursor := s.NewCursor()
|
||||
cursor.Save() // for proper cursor placement during selection
|
||||
cursor.Hide() // hide the cursor
|
||||
defer cursor.Show() // show the cursor when we're done
|
||||
defer cursor.Restore() // clear any accessibility offsetting on exit
|
||||
|
||||
tmplData := SelectTemplateData{
|
||||
Select: *s,
|
||||
SelectedIndex: idx,
|
||||
Description: s.Description,
|
||||
ShowHelp: s.showingHelp,
|
||||
PageEntries: opts,
|
||||
Config: config,
|
||||
}
|
||||
|
||||
// ask the question
|
||||
err := s.RenderWithCursorOffset(SelectQuestionTemplate, tmplData, opts, idx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
rr := s.NewRuneReader()
|
||||
_ = rr.SetTermMode()
|
||||
defer func() {
|
||||
_ = rr.RestoreTermMode()
|
||||
}()
|
||||
|
||||
// start waiting for input
|
||||
for {
|
||||
r, _, err := rr.ReadRune()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if r == terminal.KeyInterrupt {
|
||||
return "", terminal.InterruptErr
|
||||
}
|
||||
if r == terminal.KeyEndTransmission {
|
||||
break
|
||||
}
|
||||
if s.OnChange(r, config) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
options := s.filterOptions(config)
|
||||
s.filter = ""
|
||||
s.FilterMessage = ""
|
||||
|
||||
if s.selectedIndex < len(options) {
|
||||
return options[s.selectedIndex], err
|
||||
}
|
||||
|
||||
return options[0], err
|
||||
}
|
||||
|
||||
func (s *Select) Cleanup(config *PromptConfig, val interface{}) error {
|
||||
cursor := s.NewCursor()
|
||||
cursor.Restore()
|
||||
return s.Render(
|
||||
SelectQuestionTemplate,
|
||||
SelectTemplateData{
|
||||
Select: *s,
|
||||
Answer: val.(core.OptionAnswer).Value,
|
||||
ShowAnswer: true,
|
||||
Description: s.Description,
|
||||
Config: config,
|
||||
},
|
||||
)
|
||||
}
|
||||
-474
@@ -1,474 +0,0 @@
|
||||
package survey
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2/core"
|
||||
"github.com/AlecAivazis/survey/v2/terminal"
|
||||
)
|
||||
|
||||
// DefaultAskOptions is the default options on ask, using the OS stdio.
|
||||
func defaultAskOptions() *AskOptions {
|
||||
return &AskOptions{
|
||||
Stdio: terminal.Stdio{
|
||||
In: os.Stdin,
|
||||
Out: os.Stdout,
|
||||
Err: os.Stderr,
|
||||
},
|
||||
PromptConfig: PromptConfig{
|
||||
PageSize: 7,
|
||||
HelpInput: "?",
|
||||
SuggestInput: "tab",
|
||||
Icons: IconSet{
|
||||
Error: Icon{
|
||||
Text: "X",
|
||||
Format: "red",
|
||||
},
|
||||
Help: Icon{
|
||||
Text: "?",
|
||||
Format: "cyan",
|
||||
},
|
||||
Question: Icon{
|
||||
Text: "?",
|
||||
Format: "green+hb",
|
||||
},
|
||||
MarkedOption: Icon{
|
||||
Text: "[x]",
|
||||
Format: "green",
|
||||
},
|
||||
UnmarkedOption: Icon{
|
||||
Text: "[ ]",
|
||||
Format: "default+hb",
|
||||
},
|
||||
SelectFocus: Icon{
|
||||
Text: ">",
|
||||
Format: "cyan+b",
|
||||
},
|
||||
},
|
||||
Filter: func(filter string, value string, index int) (include bool) {
|
||||
filter = strings.ToLower(filter)
|
||||
|
||||
// include this option if it matches
|
||||
return strings.Contains(strings.ToLower(value), filter)
|
||||
},
|
||||
KeepFilter: false,
|
||||
ShowCursor: false,
|
||||
RemoveSelectAll: false,
|
||||
RemoveSelectNone: false,
|
||||
HideCharacter: '*',
|
||||
},
|
||||
}
|
||||
}
|
||||
func defaultPromptConfig() *PromptConfig {
|
||||
return &defaultAskOptions().PromptConfig
|
||||
}
|
||||
|
||||
func defaultIcons() *IconSet {
|
||||
return &defaultPromptConfig().Icons
|
||||
}
|
||||
|
||||
// OptionAnswer is an ergonomic alias for core.OptionAnswer
|
||||
type OptionAnswer = core.OptionAnswer
|
||||
|
||||
// Icon holds the text and format to show for a particular icon
|
||||
type Icon struct {
|
||||
Text string
|
||||
Format string
|
||||
}
|
||||
|
||||
// IconSet holds the icons to use for various prompts
|
||||
type IconSet struct {
|
||||
HelpInput Icon
|
||||
Error Icon
|
||||
Help Icon
|
||||
Question Icon
|
||||
MarkedOption Icon
|
||||
UnmarkedOption Icon
|
||||
SelectFocus Icon
|
||||
}
|
||||
|
||||
// Validator is a function passed to a Question after a user has provided a response.
|
||||
// If the function returns an error, then the user will be prompted again for another
|
||||
// response.
|
||||
type Validator func(ans interface{}) error
|
||||
|
||||
// Transformer is a function passed to a Question after a user has provided a response.
|
||||
// The function can be used to implement a custom logic that will result to return
|
||||
// a different representation of the given answer.
|
||||
//
|
||||
// Look `TransformString`, `ToLower` `Title` and `ComposeTransformers` for more.
|
||||
type Transformer func(ans interface{}) (newAns interface{})
|
||||
|
||||
// Question is the core data structure for a survey questionnaire.
|
||||
type Question struct {
|
||||
Name string
|
||||
Prompt Prompt
|
||||
Validate Validator
|
||||
Transform Transformer
|
||||
}
|
||||
|
||||
// PromptConfig holds the global configuration for a prompt
|
||||
type PromptConfig struct {
|
||||
PageSize int
|
||||
Icons IconSet
|
||||
HelpInput string
|
||||
SuggestInput string
|
||||
Filter func(filter string, option string, index int) bool
|
||||
KeepFilter bool
|
||||
ShowCursor bool
|
||||
RemoveSelectAll bool
|
||||
RemoveSelectNone bool
|
||||
HideCharacter rune
|
||||
}
|
||||
|
||||
// Prompt is the primary interface for the objects that can take user input
|
||||
// and return a response.
|
||||
type Prompt interface {
|
||||
Prompt(config *PromptConfig) (interface{}, error)
|
||||
Cleanup(*PromptConfig, interface{}) error
|
||||
Error(*PromptConfig, error) error
|
||||
}
|
||||
|
||||
// PromptAgainer Interface for Prompts that support prompting again after invalid input
|
||||
type PromptAgainer interface {
|
||||
PromptAgain(config *PromptConfig, invalid interface{}, err error) (interface{}, error)
|
||||
}
|
||||
|
||||
// AskOpt allows setting optional ask options.
|
||||
type AskOpt func(options *AskOptions) error
|
||||
|
||||
// AskOptions provides additional options on ask.
|
||||
type AskOptions struct {
|
||||
Stdio terminal.Stdio
|
||||
Validators []Validator
|
||||
PromptConfig PromptConfig
|
||||
}
|
||||
|
||||
// WithStdio specifies the standard input, output and error files survey
|
||||
// interacts with. By default, these are os.Stdin, os.Stdout, and os.Stderr.
|
||||
func WithStdio(in terminal.FileReader, out terminal.FileWriter, err io.Writer) AskOpt {
|
||||
return func(options *AskOptions) error {
|
||||
options.Stdio.In = in
|
||||
options.Stdio.Out = out
|
||||
options.Stdio.Err = err
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithFilter specifies the default filter to use when asking questions.
|
||||
func WithFilter(filter func(filter string, value string, index int) (include bool)) AskOpt {
|
||||
return func(options *AskOptions) error {
|
||||
// save the filter internally
|
||||
options.PromptConfig.Filter = filter
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithKeepFilter sets the if the filter is kept after selections
|
||||
func WithKeepFilter(KeepFilter bool) AskOpt {
|
||||
return func(options *AskOptions) error {
|
||||
// set the page size
|
||||
options.PromptConfig.KeepFilter = KeepFilter
|
||||
|
||||
// nothing went wrong
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithRemoveSelectAll remove the select all option in Multiselect
|
||||
func WithRemoveSelectAll() AskOpt {
|
||||
return func(options *AskOptions) error {
|
||||
options.PromptConfig.RemoveSelectAll = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithRemoveSelectNone remove the select none/unselect all in Multiselect
|
||||
func WithRemoveSelectNone() AskOpt {
|
||||
return func(options *AskOptions) error {
|
||||
options.PromptConfig.RemoveSelectNone = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithValidator specifies a validator to use while prompting the user
|
||||
func WithValidator(v Validator) AskOpt {
|
||||
return func(options *AskOptions) error {
|
||||
// add the provided validator to the list
|
||||
options.Validators = append(options.Validators, v)
|
||||
|
||||
// nothing went wrong
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type wantsStdio interface {
|
||||
WithStdio(terminal.Stdio)
|
||||
}
|
||||
|
||||
// WithPageSize sets the default page size used by prompts
|
||||
func WithPageSize(pageSize int) AskOpt {
|
||||
return func(options *AskOptions) error {
|
||||
// set the page size
|
||||
options.PromptConfig.PageSize = pageSize
|
||||
|
||||
// nothing went wrong
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithHelpInput changes the character that prompts look for to give the user helpful information.
|
||||
func WithHelpInput(r rune) AskOpt {
|
||||
return func(options *AskOptions) error {
|
||||
// set the input character
|
||||
options.PromptConfig.HelpInput = string(r)
|
||||
|
||||
// nothing went wrong
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithIcons sets the icons that will be used when prompting the user
|
||||
func WithIcons(setIcons func(*IconSet)) AskOpt {
|
||||
return func(options *AskOptions) error {
|
||||
// update the default icons with whatever the user says
|
||||
setIcons(&options.PromptConfig.Icons)
|
||||
|
||||
// nothing went wrong
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithShowCursor sets the show cursor behavior when prompting the user
|
||||
func WithShowCursor(ShowCursor bool) AskOpt {
|
||||
return func(options *AskOptions) error {
|
||||
// set the page size
|
||||
options.PromptConfig.ShowCursor = ShowCursor
|
||||
|
||||
// nothing went wrong
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithHideCharacter sets the default character shown instead of the password for password inputs
|
||||
func WithHideCharacter(char rune) AskOpt {
|
||||
return func(options *AskOptions) error {
|
||||
// set the hide character
|
||||
options.PromptConfig.HideCharacter = char
|
||||
|
||||
// nothing went wrong
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
AskOne performs the prompt for a single prompt and asks for validation if required.
|
||||
Response types should be something that can be casted from the response type designated
|
||||
in the documentation. For example:
|
||||
|
||||
name := ""
|
||||
prompt := &survey.Input{
|
||||
Message: "name",
|
||||
}
|
||||
|
||||
survey.AskOne(prompt, &name)
|
||||
*/
|
||||
func AskOne(p Prompt, response interface{}, opts ...AskOpt) error {
|
||||
err := Ask([]*Question{{Prompt: p}}, response, opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
Ask performs the prompt loop, asking for validation when appropriate. The response
|
||||
type can be one of two options. If a struct is passed, the answer will be written to
|
||||
the field whose name matches the Name field on the corresponding question. Field types
|
||||
should be something that can be casted from the response type designated in the
|
||||
documentation. Note, a survey tag can also be used to identify a Otherwise, a
|
||||
map[string]interface{} can be passed, responses will be written to the key with the
|
||||
matching name. For example:
|
||||
|
||||
qs := []*survey.Question{
|
||||
{
|
||||
Name: "name",
|
||||
Prompt: &survey.Input{Message: "What is your name?"},
|
||||
Validate: survey.Required,
|
||||
Transform: survey.Title,
|
||||
},
|
||||
}
|
||||
|
||||
answers := struct{ Name string }{}
|
||||
|
||||
|
||||
err := survey.Ask(qs, &answers)
|
||||
*/
|
||||
func Ask(qs []*Question, response interface{}, opts ...AskOpt) error {
|
||||
// build up the configuration options
|
||||
options := defaultAskOptions()
|
||||
for _, opt := range opts {
|
||||
if opt == nil {
|
||||
continue
|
||||
}
|
||||
if err := opt(options); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// if we weren't passed a place to record the answers
|
||||
if response == nil {
|
||||
// we can't go any further
|
||||
return errors.New("cannot call Ask() with a nil reference to record the answers")
|
||||
}
|
||||
|
||||
validate := func(q *Question, val interface{}) error {
|
||||
if q.Validate != nil {
|
||||
if err := q.Validate(val); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, v := range options.Validators {
|
||||
if err := v(val); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// go over every question
|
||||
for _, q := range qs {
|
||||
// If Prompt implements controllable stdio, pass in specified stdio.
|
||||
if p, ok := q.Prompt.(wantsStdio); ok {
|
||||
p.WithStdio(options.Stdio)
|
||||
}
|
||||
|
||||
var ans interface{}
|
||||
var validationErr error
|
||||
// prompt and validation loop
|
||||
for {
|
||||
if validationErr != nil {
|
||||
if err := q.Prompt.Error(&options.PromptConfig, validationErr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
var err error
|
||||
if promptAgainer, ok := q.Prompt.(PromptAgainer); ok && validationErr != nil {
|
||||
ans, err = promptAgainer.PromptAgain(&options.PromptConfig, ans, validationErr)
|
||||
} else {
|
||||
ans, err = q.Prompt.Prompt(&options.PromptConfig)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
validationErr = validate(q, ans)
|
||||
if validationErr == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if q.Transform != nil {
|
||||
// check if we have a transformer available, if so
|
||||
// then try to acquire the new representation of the
|
||||
// answer, if the resulting answer is not nil.
|
||||
if newAns := q.Transform(ans); newAns != nil {
|
||||
ans = newAns
|
||||
}
|
||||
}
|
||||
|
||||
// tell the prompt to cleanup with the validated value
|
||||
if err := q.Prompt.Cleanup(&options.PromptConfig, ans); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add it to the map
|
||||
if err := core.WriteAnswer(response, q.Name, ans); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// return the response
|
||||
return nil
|
||||
}
|
||||
|
||||
// paginate returns a single page of choices given the page size, the total list of
|
||||
// possible choices, and the current selected index in the total list.
|
||||
func paginate(pageSize int, choices []core.OptionAnswer, sel int) ([]core.OptionAnswer, int) {
|
||||
var start, end, cursor int
|
||||
|
||||
if len(choices) < pageSize {
|
||||
// if we dont have enough options to fill a page
|
||||
start = 0
|
||||
end = len(choices)
|
||||
cursor = sel
|
||||
|
||||
} else if sel < pageSize/2 {
|
||||
// if we are in the first half page
|
||||
start = 0
|
||||
end = pageSize
|
||||
cursor = sel
|
||||
|
||||
} else if len(choices)-sel-1 < pageSize/2 {
|
||||
// if we are in the last half page
|
||||
start = len(choices) - pageSize
|
||||
end = len(choices)
|
||||
cursor = sel - start
|
||||
|
||||
} else {
|
||||
// somewhere in the middle
|
||||
above := pageSize / 2
|
||||
below := pageSize - above
|
||||
|
||||
cursor = pageSize / 2
|
||||
start = sel - above
|
||||
end = sel + below
|
||||
}
|
||||
|
||||
// return the subset we care about and the index
|
||||
return choices[start:end], cursor
|
||||
}
|
||||
|
||||
type IterableOpts interface {
|
||||
IterateOption(int, core.OptionAnswer) interface{}
|
||||
}
|
||||
|
||||
func computeCursorOffset(tmpl string, data IterableOpts, opts []core.OptionAnswer, idx, tWidth int) int {
|
||||
tmpls, err := core.GetTemplatePair(tmpl)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
t := tmpls[0]
|
||||
|
||||
renderOpt := func(ix int, opt core.OptionAnswer) string {
|
||||
var buf bytes.Buffer
|
||||
_ = t.ExecuteTemplate(&buf, "option", data.IterateOption(ix, opt))
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
offset := len(opts) - idx
|
||||
|
||||
for i, o := range opts {
|
||||
if i < idx {
|
||||
continue
|
||||
}
|
||||
renderedOpt := renderOpt(i, o)
|
||||
valWidth := utf8.RuneCount([]byte(renderedOpt))
|
||||
if valWidth > tWidth {
|
||||
splitCount := valWidth / tWidth
|
||||
if valWidth%tWidth == 0 {
|
||||
splitCount -= 1
|
||||
}
|
||||
offset += splitCount
|
||||
}
|
||||
}
|
||||
|
||||
return offset
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
Copyright (c) 2014 Takashi Kokubun
|
||||
|
||||
MIT License
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
# survey/terminal
|
||||
|
||||
This package started as a copy of [kokuban/go-ansi](http://github.com/k0kubun/go-ansi) but has since been modified to fit survey's specific needs.
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
)
|
||||
|
||||
type BufferedReader struct {
|
||||
In io.Reader
|
||||
Buffer *bytes.Buffer
|
||||
}
|
||||
|
||||
func (br *BufferedReader) Read(p []byte) (int, error) {
|
||||
n, err := br.Buffer.Read(p)
|
||||
if err != nil && err != io.EOF {
|
||||
return n, err
|
||||
} else if err == nil {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
return br.In.Read(p[n:])
|
||||
}
|
||||
-209
@@ -1,209 +0,0 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var COORDINATE_SYSTEM_BEGIN Short = 1
|
||||
|
||||
var dsrPattern = regexp.MustCompile(`\x1b\[(\d+);(\d+)R$`)
|
||||
|
||||
type Cursor struct {
|
||||
In FileReader
|
||||
Out FileWriter
|
||||
}
|
||||
|
||||
// Up moves the cursor n cells to up.
|
||||
func (c *Cursor) Up(n int) error {
|
||||
_, err := fmt.Fprintf(c.Out, "\x1b[%dA", n)
|
||||
return err
|
||||
}
|
||||
|
||||
// Down moves the cursor n cells to down.
|
||||
func (c *Cursor) Down(n int) error {
|
||||
_, err := fmt.Fprintf(c.Out, "\x1b[%dB", n)
|
||||
return err
|
||||
}
|
||||
|
||||
// Forward moves the cursor n cells to right.
|
||||
func (c *Cursor) Forward(n int) error {
|
||||
_, err := fmt.Fprintf(c.Out, "\x1b[%dC", n)
|
||||
return err
|
||||
}
|
||||
|
||||
// Back moves the cursor n cells to left.
|
||||
func (c *Cursor) Back(n int) error {
|
||||
_, err := fmt.Fprintf(c.Out, "\x1b[%dD", n)
|
||||
return err
|
||||
}
|
||||
|
||||
// NextLine moves cursor to beginning of the line n lines down.
|
||||
func (c *Cursor) NextLine(n int) error {
|
||||
if err := c.Down(1); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.HorizontalAbsolute(0)
|
||||
}
|
||||
|
||||
// PreviousLine moves cursor to beginning of the line n lines up.
|
||||
func (c *Cursor) PreviousLine(n int) error {
|
||||
if err := c.Up(1); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.HorizontalAbsolute(0)
|
||||
}
|
||||
|
||||
// HorizontalAbsolute moves cursor horizontally to x.
|
||||
func (c *Cursor) HorizontalAbsolute(x int) error {
|
||||
_, err := fmt.Fprintf(c.Out, "\x1b[%dG", x)
|
||||
return err
|
||||
}
|
||||
|
||||
// Show shows the cursor.
|
||||
func (c *Cursor) Show() error {
|
||||
_, err := fmt.Fprint(c.Out, "\x1b[?25h")
|
||||
return err
|
||||
}
|
||||
|
||||
// Hide hide the cursor.
|
||||
func (c *Cursor) Hide() error {
|
||||
_, err := fmt.Fprint(c.Out, "\x1b[?25l")
|
||||
return err
|
||||
}
|
||||
|
||||
// move moves the cursor to a specific x,y location.
|
||||
func (c *Cursor) move(x int, y int) error {
|
||||
_, err := fmt.Fprintf(c.Out, "\x1b[%d;%df", x, y)
|
||||
return err
|
||||
}
|
||||
|
||||
// Save saves the current position
|
||||
func (c *Cursor) Save() error {
|
||||
_, err := fmt.Fprint(c.Out, "\x1b7")
|
||||
return err
|
||||
}
|
||||
|
||||
// Restore restores the saved position of the cursor
|
||||
func (c *Cursor) Restore() error {
|
||||
_, err := fmt.Fprint(c.Out, "\x1b8")
|
||||
return err
|
||||
}
|
||||
|
||||
// for comparability purposes between windows
|
||||
// in unix we need to print out a new line on some terminals
|
||||
func (c *Cursor) MoveNextLine(cur *Coord, terminalSize *Coord) error {
|
||||
if cur.Y == terminalSize.Y {
|
||||
if _, err := fmt.Fprintln(c.Out); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return c.NextLine(1)
|
||||
}
|
||||
|
||||
// Location returns the current location of the cursor in the terminal
|
||||
func (c *Cursor) Location(buf *bytes.Buffer) (*Coord, error) {
|
||||
// ANSI escape sequence for DSR - Device Status Report
|
||||
// https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
|
||||
if _, err := fmt.Fprint(c.Out, "\x1b[6n"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// There may be input in Stdin prior to CursorLocation so make sure we don't
|
||||
// drop those bytes.
|
||||
var loc []int
|
||||
var match string
|
||||
for loc == nil {
|
||||
// Reports the cursor position (CPR) to the application as (as though typed at
|
||||
// the keyboard) ESC[n;mR, where n is the row and m is the column.
|
||||
reader := bufio.NewReader(c.In)
|
||||
text, err := reader.ReadSlice(byte('R'))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
loc = dsrPattern.FindStringIndex(string(text))
|
||||
if loc == nil {
|
||||
// After reading slice to byte 'R', the bufio Reader may have read more
|
||||
// bytes into its internal buffer which will be discarded on next ReadSlice.
|
||||
// We create a temporary buffer to read the remaining buffered slice and
|
||||
// write them to output buffer.
|
||||
buffered := make([]byte, reader.Buffered())
|
||||
_, err = io.ReadFull(reader, buffered)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Stdin contains R that doesn't match DSR, so pass the bytes along to
|
||||
// output buffer.
|
||||
buf.Write(text)
|
||||
buf.Write(buffered)
|
||||
} else {
|
||||
// Write the non-matching leading bytes to output buffer.
|
||||
buf.Write(text[:loc[0]])
|
||||
|
||||
// Save the matching bytes to extract the row and column of the cursor.
|
||||
match = string(text[loc[0]:loc[1]])
|
||||
}
|
||||
}
|
||||
|
||||
matches := dsrPattern.FindStringSubmatch(string(match))
|
||||
if len(matches) != 3 {
|
||||
return nil, fmt.Errorf("incorrect number of matches: %d", len(matches))
|
||||
}
|
||||
|
||||
col, err := strconv.Atoi(matches[2])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
row, err := strconv.Atoi(matches[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Coord{Short(col), Short(row)}, nil
|
||||
}
|
||||
|
||||
func (cur Coord) CursorIsAtLineEnd(size *Coord) bool {
|
||||
return cur.X == size.X
|
||||
}
|
||||
|
||||
func (cur Coord) CursorIsAtLineBegin() bool {
|
||||
return cur.X == COORDINATE_SYSTEM_BEGIN
|
||||
}
|
||||
|
||||
// Size returns the height and width of the terminal.
|
||||
func (c *Cursor) Size(buf *bytes.Buffer) (*Coord, error) {
|
||||
// the general approach here is to move the cursor to the very bottom
|
||||
// of the terminal, ask for the current location and then move the
|
||||
// cursor back where we started
|
||||
|
||||
// hide the cursor (so it doesn't blink when getting the size of the terminal)
|
||||
c.Hide()
|
||||
defer c.Show()
|
||||
|
||||
// save the current location of the cursor
|
||||
c.Save()
|
||||
defer c.Restore()
|
||||
|
||||
// move the cursor to the very bottom of the terminal
|
||||
c.move(999, 999)
|
||||
|
||||
// ask for the current location
|
||||
bottom, err := c.Location(buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// since the bottom was calculated in the lower right corner, it
|
||||
// is the dimensions we are looking for
|
||||
return bottom, nil
|
||||
}
|
||||
-164
@@ -1,164 +0,0 @@
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var COORDINATE_SYSTEM_BEGIN Short = 0
|
||||
|
||||
// shared variable to save the cursor location from CursorSave()
|
||||
var cursorLoc Coord
|
||||
|
||||
type Cursor struct {
|
||||
In FileReader
|
||||
Out FileWriter
|
||||
}
|
||||
|
||||
func (c *Cursor) Up(n int) error {
|
||||
return c.cursorMove(0, n)
|
||||
}
|
||||
|
||||
func (c *Cursor) Down(n int) error {
|
||||
return c.cursorMove(0, -1*n)
|
||||
}
|
||||
|
||||
func (c *Cursor) Forward(n int) error {
|
||||
return c.cursorMove(n, 0)
|
||||
}
|
||||
|
||||
func (c *Cursor) Back(n int) error {
|
||||
return c.cursorMove(-1*n, 0)
|
||||
}
|
||||
|
||||
// save the cursor location
|
||||
func (c *Cursor) Save() error {
|
||||
loc, err := c.Location(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cursorLoc = *loc
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cursor) Restore() error {
|
||||
handle := syscall.Handle(c.Out.Fd())
|
||||
// restore it to the original position
|
||||
_, _, err := procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursorLoc))))
|
||||
return normalizeError(err)
|
||||
}
|
||||
|
||||
func (cur Coord) CursorIsAtLineEnd(size *Coord) bool {
|
||||
return cur.X == size.X
|
||||
}
|
||||
|
||||
func (cur Coord) CursorIsAtLineBegin() bool {
|
||||
return cur.X == 0
|
||||
}
|
||||
|
||||
func (c *Cursor) cursorMove(x int, y int) error {
|
||||
handle := syscall.Handle(c.Out.Fd())
|
||||
|
||||
var csbi consoleScreenBufferInfo
|
||||
if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var cursor Coord
|
||||
cursor.X = csbi.cursorPosition.X + Short(x)
|
||||
cursor.Y = csbi.cursorPosition.Y + Short(y)
|
||||
|
||||
_, _, err := procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursor))))
|
||||
return normalizeError(err)
|
||||
}
|
||||
|
||||
func (c *Cursor) NextLine(n int) error {
|
||||
if err := c.Up(n); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.HorizontalAbsolute(0)
|
||||
}
|
||||
|
||||
func (c *Cursor) PreviousLine(n int) error {
|
||||
if err := c.Down(n); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.HorizontalAbsolute(0)
|
||||
}
|
||||
|
||||
// for comparability purposes between windows
|
||||
// in windows we don't have to print out a new line
|
||||
func (c *Cursor) MoveNextLine(cur *Coord, terminalSize *Coord) error {
|
||||
return c.NextLine(1)
|
||||
}
|
||||
|
||||
func (c *Cursor) HorizontalAbsolute(x int) error {
|
||||
handle := syscall.Handle(c.Out.Fd())
|
||||
|
||||
var csbi consoleScreenBufferInfo
|
||||
if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var cursor Coord
|
||||
cursor.X = Short(x)
|
||||
cursor.Y = csbi.cursorPosition.Y
|
||||
|
||||
if csbi.size.X < cursor.X {
|
||||
cursor.X = csbi.size.X
|
||||
}
|
||||
|
||||
_, _, err := procSetConsoleCursorPosition.Call(uintptr(handle), uintptr(*(*int32)(unsafe.Pointer(&cursor))))
|
||||
return normalizeError(err)
|
||||
}
|
||||
|
||||
func (c *Cursor) Show() error {
|
||||
handle := syscall.Handle(c.Out.Fd())
|
||||
|
||||
var cci consoleCursorInfo
|
||||
if _, _, err := procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci))); normalizeError(err) != nil {
|
||||
return err
|
||||
}
|
||||
cci.visible = 1
|
||||
|
||||
_, _, err := procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
|
||||
return normalizeError(err)
|
||||
}
|
||||
|
||||
func (c *Cursor) Hide() error {
|
||||
handle := syscall.Handle(c.Out.Fd())
|
||||
|
||||
var cci consoleCursorInfo
|
||||
if _, _, err := procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci))); normalizeError(err) != nil {
|
||||
return err
|
||||
}
|
||||
cci.visible = 0
|
||||
|
||||
_, _, err := procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&cci)))
|
||||
return normalizeError(err)
|
||||
}
|
||||
|
||||
func (c *Cursor) Location(buf *bytes.Buffer) (*Coord, error) {
|
||||
handle := syscall.Handle(c.Out.Fd())
|
||||
|
||||
var csbi consoleScreenBufferInfo
|
||||
if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &csbi.cursorPosition, nil
|
||||
}
|
||||
|
||||
func (c *Cursor) Size(buf *bytes.Buffer) (*Coord, error) {
|
||||
handle := syscall.Handle(c.Out.Fd())
|
||||
|
||||
var csbi consoleScreenBufferInfo
|
||||
if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil {
|
||||
return nil, err
|
||||
}
|
||||
// windows' coordinate system begins at (0, 0)
|
||||
csbi.size.X--
|
||||
csbi.size.Y--
|
||||
return &csbi.size, nil
|
||||
}
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
package terminal
|
||||
|
||||
type EraseLineMode int
|
||||
|
||||
const (
|
||||
ERASE_LINE_END EraseLineMode = iota
|
||||
ERASE_LINE_START
|
||||
ERASE_LINE_ALL
|
||||
)
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func EraseLine(out FileWriter, mode EraseLineMode) error {
|
||||
_, err := fmt.Fprintf(out, "\x1b[%dK", mode)
|
||||
return err
|
||||
}
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func EraseLine(out FileWriter, mode EraseLineMode) error {
|
||||
handle := syscall.Handle(out.Fd())
|
||||
|
||||
var csbi consoleScreenBufferInfo
|
||||
if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var w uint32
|
||||
var x Short
|
||||
cursor := csbi.cursorPosition
|
||||
switch mode {
|
||||
case ERASE_LINE_END:
|
||||
x = csbi.size.X
|
||||
case ERASE_LINE_START:
|
||||
x = 0
|
||||
case ERASE_LINE_ALL:
|
||||
cursor.X = 0
|
||||
x = csbi.size.X
|
||||
}
|
||||
|
||||
_, _, err := procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(x), uintptr(*(*int32)(unsafe.Pointer(&cursor))), uintptr(unsafe.Pointer(&w)))
|
||||
return normalizeError(err)
|
||||
}
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
//lint:ignore ST1012 keeping old name for backwards compatibility
|
||||
InterruptErr = errors.New("interrupt")
|
||||
)
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// NewAnsiStdout returns special stdout, which converts escape sequences to Windows API calls
|
||||
// on Windows environment.
|
||||
func NewAnsiStdout(out FileWriter) io.Writer {
|
||||
return out
|
||||
}
|
||||
|
||||
// NewAnsiStderr returns special stderr, which converts escape sequences to Windows API calls
|
||||
// on Windows environment.
|
||||
func NewAnsiStderr(out FileWriter) io.Writer {
|
||||
return out
|
||||
}
|
||||
-253
@@ -1,253 +0,0 @@
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/mattn/go-isatty"
|
||||
)
|
||||
|
||||
const (
|
||||
foregroundBlue = 0x1
|
||||
foregroundGreen = 0x2
|
||||
foregroundRed = 0x4
|
||||
foregroundIntensity = 0x8
|
||||
foregroundMask = (foregroundRed | foregroundBlue | foregroundGreen | foregroundIntensity)
|
||||
backgroundBlue = 0x10
|
||||
backgroundGreen = 0x20
|
||||
backgroundRed = 0x40
|
||||
backgroundIntensity = 0x80
|
||||
backgroundMask = (backgroundRed | backgroundBlue | backgroundGreen | backgroundIntensity)
|
||||
)
|
||||
|
||||
type Writer struct {
|
||||
out FileWriter
|
||||
handle syscall.Handle
|
||||
orgAttr word
|
||||
}
|
||||
|
||||
func NewAnsiStdout(out FileWriter) io.Writer {
|
||||
var csbi consoleScreenBufferInfo
|
||||
if !isatty.IsTerminal(out.Fd()) {
|
||||
return out
|
||||
}
|
||||
handle := syscall.Handle(out.Fd())
|
||||
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
|
||||
return &Writer{out: out, handle: handle, orgAttr: csbi.attributes}
|
||||
}
|
||||
|
||||
func NewAnsiStderr(out FileWriter) io.Writer {
|
||||
var csbi consoleScreenBufferInfo
|
||||
if !isatty.IsTerminal(out.Fd()) {
|
||||
return out
|
||||
}
|
||||
handle := syscall.Handle(out.Fd())
|
||||
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
|
||||
return &Writer{out: out, handle: handle, orgAttr: csbi.attributes}
|
||||
}
|
||||
|
||||
func (w *Writer) Write(data []byte) (n int, err error) {
|
||||
r := bytes.NewReader(data)
|
||||
|
||||
for {
|
||||
var ch rune
|
||||
var size int
|
||||
ch, size, err = r.ReadRune()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
n += size
|
||||
|
||||
switch ch {
|
||||
case '\x1b':
|
||||
size, err = w.handleEscape(r)
|
||||
n += size
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
default:
|
||||
_, err = fmt.Fprint(w.out, string(ch))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Writer) handleEscape(r *bytes.Reader) (n int, err error) {
|
||||
buf := make([]byte, 0, 10)
|
||||
buf = append(buf, "\x1b"...)
|
||||
|
||||
var ch rune
|
||||
var size int
|
||||
// Check '[' continues after \x1b
|
||||
ch, size, err = r.ReadRune()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
fmt.Fprint(w.out, string(buf))
|
||||
return
|
||||
}
|
||||
n += size
|
||||
if ch != '[' {
|
||||
fmt.Fprint(w.out, string(buf))
|
||||
return
|
||||
}
|
||||
|
||||
// Parse escape code
|
||||
var code rune
|
||||
argBuf := make([]byte, 0, 10)
|
||||
for {
|
||||
ch, size, err = r.ReadRune()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
fmt.Fprint(w.out, string(buf))
|
||||
return
|
||||
}
|
||||
n += size
|
||||
if ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') {
|
||||
code = ch
|
||||
break
|
||||
}
|
||||
argBuf = append(argBuf, string(ch)...)
|
||||
}
|
||||
|
||||
err = w.applyEscapeCode(buf, string(argBuf), code)
|
||||
return
|
||||
}
|
||||
|
||||
func (w *Writer) applyEscapeCode(buf []byte, arg string, code rune) error {
|
||||
c := &Cursor{Out: w.out}
|
||||
|
||||
switch arg + string(code) {
|
||||
case "?25h":
|
||||
return c.Show()
|
||||
case "?25l":
|
||||
return c.Hide()
|
||||
}
|
||||
|
||||
if code >= 'A' && code <= 'G' {
|
||||
if n, err := strconv.Atoi(arg); err == nil {
|
||||
switch code {
|
||||
case 'A':
|
||||
return c.Up(n)
|
||||
case 'B':
|
||||
return c.Down(n)
|
||||
case 'C':
|
||||
return c.Forward(n)
|
||||
case 'D':
|
||||
return c.Back(n)
|
||||
case 'E':
|
||||
return c.NextLine(n)
|
||||
case 'F':
|
||||
return c.PreviousLine(n)
|
||||
case 'G':
|
||||
return c.HorizontalAbsolute(n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch code {
|
||||
case 'm':
|
||||
return w.applySelectGraphicRendition(arg)
|
||||
default:
|
||||
buf = append(buf, string(code)...)
|
||||
_, err := fmt.Fprint(w.out, string(buf))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Original implementation: https://github.com/mattn/go-colorable
|
||||
func (w *Writer) applySelectGraphicRendition(arg string) error {
|
||||
if arg == "" {
|
||||
_, _, err := procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(w.orgAttr))
|
||||
return normalizeError(err)
|
||||
}
|
||||
|
||||
var csbi consoleScreenBufferInfo
|
||||
if _, _, err := procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))); normalizeError(err) != nil {
|
||||
return err
|
||||
}
|
||||
attr := csbi.attributes
|
||||
|
||||
for _, param := range strings.Split(arg, ";") {
|
||||
n, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case n == 0 || n == 100:
|
||||
attr = w.orgAttr
|
||||
case 1 <= n && n <= 5:
|
||||
attr |= foregroundIntensity
|
||||
case 30 <= n && n <= 37:
|
||||
attr = (attr & backgroundMask)
|
||||
if (n-30)&1 != 0 {
|
||||
attr |= foregroundRed
|
||||
}
|
||||
if (n-30)&2 != 0 {
|
||||
attr |= foregroundGreen
|
||||
}
|
||||
if (n-30)&4 != 0 {
|
||||
attr |= foregroundBlue
|
||||
}
|
||||
case 40 <= n && n <= 47:
|
||||
attr = (attr & foregroundMask)
|
||||
if (n-40)&1 != 0 {
|
||||
attr |= backgroundRed
|
||||
}
|
||||
if (n-40)&2 != 0 {
|
||||
attr |= backgroundGreen
|
||||
}
|
||||
if (n-40)&4 != 0 {
|
||||
attr |= backgroundBlue
|
||||
}
|
||||
case 90 <= n && n <= 97:
|
||||
attr = (attr & backgroundMask)
|
||||
attr |= foregroundIntensity
|
||||
if (n-90)&1 != 0 {
|
||||
attr |= foregroundRed
|
||||
}
|
||||
if (n-90)&2 != 0 {
|
||||
attr |= foregroundGreen
|
||||
}
|
||||
if (n-90)&4 != 0 {
|
||||
attr |= foregroundBlue
|
||||
}
|
||||
case 100 <= n && n <= 107:
|
||||
attr = (attr & foregroundMask)
|
||||
attr |= backgroundIntensity
|
||||
if (n-100)&1 != 0 {
|
||||
attr |= backgroundRed
|
||||
}
|
||||
if (n-100)&2 != 0 {
|
||||
attr |= backgroundGreen
|
||||
}
|
||||
if (n-100)&4 != 0 {
|
||||
attr |= backgroundBlue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_, _, err := procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(attr))
|
||||
return normalizeError(err)
|
||||
}
|
||||
|
||||
func normalizeError(err error) error {
|
||||
if syserr, ok := err.(syscall.Errno); ok && syserr == 0 {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
-417
@@ -1,417 +0,0 @@
|
||||
package terminal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"unicode"
|
||||
|
||||
"golang.org/x/text/width"
|
||||
)
|
||||
|
||||
type RuneReader struct {
|
||||
stdio Stdio
|
||||
state runeReaderState
|
||||
}
|
||||
|
||||
func NewRuneReader(stdio Stdio) *RuneReader {
|
||||
return &RuneReader{
|
||||
stdio: stdio,
|
||||
state: newRuneReaderState(stdio.In),
|
||||
}
|
||||
}
|
||||
|
||||
func (rr *RuneReader) printChar(char rune, mask rune) error {
|
||||
// if we don't need to mask the input
|
||||
if mask == 0 {
|
||||
// just print the character the user pressed
|
||||
_, err := fmt.Fprintf(rr.stdio.Out, "%c", char)
|
||||
return err
|
||||
}
|
||||
// otherwise print the mask we were given
|
||||
_, err := fmt.Fprintf(rr.stdio.Out, "%c", mask)
|
||||
return err
|
||||
}
|
||||
|
||||
type OnRuneFn func(rune, []rune) ([]rune, bool, error)
|
||||
|
||||
func (rr *RuneReader) ReadLine(mask rune, onRunes ...OnRuneFn) ([]rune, error) {
|
||||
return rr.ReadLineWithDefault(mask, []rune{}, onRunes...)
|
||||
}
|
||||
|
||||
func (rr *RuneReader) ReadLineWithDefault(mask rune, d []rune, onRunes ...OnRuneFn) ([]rune, error) {
|
||||
line := []rune{}
|
||||
// we only care about horizontal displacements from the origin so start counting at 0
|
||||
index := 0
|
||||
|
||||
cursor := &Cursor{
|
||||
In: rr.stdio.In,
|
||||
Out: rr.stdio.Out,
|
||||
}
|
||||
|
||||
onRune := func(r rune, line []rune) ([]rune, bool, error) {
|
||||
return line, false, nil
|
||||
}
|
||||
|
||||
// if the user pressed a key the caller was interested in capturing
|
||||
if len(onRunes) > 0 {
|
||||
onRune = onRunes[0]
|
||||
}
|
||||
|
||||
// we get the terminal width and height (if resized after this point the property might become invalid)
|
||||
terminalSize, _ := cursor.Size(rr.Buffer())
|
||||
// we set the current location of the cursor once
|
||||
cursorCurrent, _ := cursor.Location(rr.Buffer())
|
||||
|
||||
increment := func() {
|
||||
if cursorCurrent.CursorIsAtLineEnd(terminalSize) {
|
||||
cursorCurrent.X = COORDINATE_SYSTEM_BEGIN
|
||||
cursorCurrent.Y++
|
||||
} else {
|
||||
cursorCurrent.X++
|
||||
}
|
||||
}
|
||||
decrement := func() {
|
||||
if cursorCurrent.CursorIsAtLineBegin() {
|
||||
cursorCurrent.X = terminalSize.X
|
||||
cursorCurrent.Y--
|
||||
} else {
|
||||
cursorCurrent.X--
|
||||
}
|
||||
}
|
||||
|
||||
if len(d) > 0 {
|
||||
index = len(d)
|
||||
if _, err := fmt.Fprint(rr.stdio.Out, string(d)); err != nil {
|
||||
return d, err
|
||||
}
|
||||
line = d
|
||||
for range d {
|
||||
increment()
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
// wait for some input
|
||||
r, _, err := rr.ReadRune()
|
||||
if err != nil {
|
||||
return line, err
|
||||
}
|
||||
|
||||
if l, stop, err := onRune(r, line); stop || err != nil {
|
||||
return l, err
|
||||
}
|
||||
|
||||
// if the user pressed enter or some other newline/termination like ctrl+d
|
||||
if r == '\r' || r == '\n' || r == KeyEndTransmission {
|
||||
// delete what's printed out on the console screen (cleanup)
|
||||
for index > 0 {
|
||||
if cursorCurrent.CursorIsAtLineBegin() {
|
||||
EraseLine(rr.stdio.Out, ERASE_LINE_END)
|
||||
cursor.PreviousLine(1)
|
||||
cursor.Forward(int(terminalSize.X))
|
||||
} else {
|
||||
cursor.Back(1)
|
||||
}
|
||||
decrement()
|
||||
index--
|
||||
}
|
||||
// move the cursor the a new line
|
||||
cursor.MoveNextLine(cursorCurrent, terminalSize)
|
||||
|
||||
// we're done processing the input
|
||||
return line, nil
|
||||
}
|
||||
// if the user interrupts (ie with ctrl+c)
|
||||
if r == KeyInterrupt {
|
||||
// go to the beginning of the next line
|
||||
if _, err := fmt.Fprint(rr.stdio.Out, "\r\n"); err != nil {
|
||||
return line, err
|
||||
}
|
||||
|
||||
// we're done processing the input, and treat interrupt like an error
|
||||
return line, InterruptErr
|
||||
}
|
||||
|
||||
// allow for backspace/delete editing of inputs
|
||||
if r == KeyBackspace || r == KeyDelete {
|
||||
// and we're not at the beginning of the line
|
||||
if index > 0 && len(line) > 0 {
|
||||
// if we are at the end of the word
|
||||
if index == len(line) {
|
||||
// just remove the last letter from the internal representation
|
||||
// also count the number of cells the rune before the cursor occupied
|
||||
cells := runeWidth(line[len(line)-1])
|
||||
line = line[:len(line)-1]
|
||||
// go back one
|
||||
if cursorCurrent.X == 1 {
|
||||
cursor.PreviousLine(1)
|
||||
cursor.Forward(int(terminalSize.X))
|
||||
} else {
|
||||
cursor.Back(cells)
|
||||
}
|
||||
|
||||
// clear the rest of the line
|
||||
EraseLine(rr.stdio.Out, ERASE_LINE_END)
|
||||
} else {
|
||||
// we need to remove a character from the middle of the word
|
||||
|
||||
cells := runeWidth(line[index-1])
|
||||
|
||||
// remove the current index from the list
|
||||
line = append(line[:index-1], line[index:]...)
|
||||
|
||||
// save the current position of the cursor, as we have to move the cursor one back to erase the current symbol
|
||||
// and then move the cursor for each symbol in line[index-1:] to print it out, afterwards we want to restore
|
||||
// the cursor to its previous location.
|
||||
cursor.Save()
|
||||
|
||||
// clear the rest of the line
|
||||
cursor.Back(cells)
|
||||
|
||||
// print what comes after
|
||||
for _, char := range line[index-1:] {
|
||||
//Erase symbols which are left over from older print
|
||||
EraseLine(rr.stdio.Out, ERASE_LINE_END)
|
||||
// print characters to the new line appropriately
|
||||
if err := rr.printChar(char, mask); err != nil {
|
||||
return line, err
|
||||
}
|
||||
}
|
||||
// erase what's left over from last print
|
||||
if cursorCurrent.Y < terminalSize.Y {
|
||||
cursor.NextLine(1)
|
||||
EraseLine(rr.stdio.Out, ERASE_LINE_END)
|
||||
}
|
||||
// restore cursor
|
||||
cursor.Restore()
|
||||
if cursorCurrent.CursorIsAtLineBegin() {
|
||||
cursor.PreviousLine(1)
|
||||
cursor.Forward(int(terminalSize.X))
|
||||
} else {
|
||||
cursor.Back(cells)
|
||||
}
|
||||
}
|
||||
|
||||
// decrement the index
|
||||
index--
|
||||
decrement()
|
||||
} else {
|
||||
// otherwise the user pressed backspace while at the beginning of the line
|
||||
_ = soundBell(rr.stdio.Out)
|
||||
}
|
||||
|
||||
// we're done processing this key
|
||||
continue
|
||||
}
|
||||
|
||||
// if the left arrow is pressed
|
||||
if r == KeyArrowLeft {
|
||||
// if we have space to the left
|
||||
if index > 0 {
|
||||
//move the cursor to the prev line if necessary
|
||||
if cursorCurrent.CursorIsAtLineBegin() {
|
||||
cursor.PreviousLine(1)
|
||||
cursor.Forward(int(terminalSize.X))
|
||||
} else {
|
||||
cursor.Back(runeWidth(line[index-1]))
|
||||
}
|
||||
//decrement the index
|
||||
index--
|
||||
decrement()
|
||||
|
||||
} else {
|
||||
// otherwise we are at the beginning of where we started reading lines
|
||||
// sound the bell
|
||||
_ = soundBell(rr.stdio.Out)
|
||||
}
|
||||
|
||||
// we're done processing this key press
|
||||
continue
|
||||
}
|
||||
|
||||
// if the right arrow is pressed
|
||||
if r == KeyArrowRight {
|
||||
// if we have space to the right
|
||||
if index < len(line) {
|
||||
// move the cursor to the next line if necessary
|
||||
if cursorCurrent.CursorIsAtLineEnd(terminalSize) {
|
||||
cursor.NextLine(1)
|
||||
} else {
|
||||
cursor.Forward(runeWidth(line[index]))
|
||||
}
|
||||
index++
|
||||
increment()
|
||||
|
||||
} else {
|
||||
// otherwise we are at the end of the word and can't go past
|
||||
// sound the bell
|
||||
_ = soundBell(rr.stdio.Out)
|
||||
}
|
||||
|
||||
// we're done processing this key press
|
||||
continue
|
||||
}
|
||||
// the user pressed one of the special keys
|
||||
if r == SpecialKeyHome {
|
||||
for index > 0 {
|
||||
if cursorCurrent.CursorIsAtLineBegin() {
|
||||
cursor.PreviousLine(1)
|
||||
cursor.Forward(int(terminalSize.X))
|
||||
cursorCurrent.Y--
|
||||
cursorCurrent.X = terminalSize.X
|
||||
} else {
|
||||
cursor.Back(runeWidth(line[index-1]))
|
||||
cursorCurrent.X -= Short(runeWidth(line[index-1]))
|
||||
}
|
||||
index--
|
||||
}
|
||||
continue
|
||||
// user pressed end
|
||||
} else if r == SpecialKeyEnd {
|
||||
for index != len(line) {
|
||||
if cursorCurrent.CursorIsAtLineEnd(terminalSize) {
|
||||
cursor.NextLine(1)
|
||||
cursorCurrent.Y++
|
||||
cursorCurrent.X = COORDINATE_SYSTEM_BEGIN
|
||||
} else {
|
||||
cursor.Forward(runeWidth(line[index]))
|
||||
cursorCurrent.X += Short(runeWidth(line[index]))
|
||||
}
|
||||
index++
|
||||
}
|
||||
continue
|
||||
// user pressed forward delete key
|
||||
} else if r == SpecialKeyDelete {
|
||||
// if index at the end of the line nothing to delete
|
||||
if index != len(line) {
|
||||
// save the current position of the cursor, as we have to erase the current symbol
|
||||
// and then move the cursor for each symbol in line[index:] to print it out, afterwards we want to restore
|
||||
// the cursor to its previous location.
|
||||
cursor.Save()
|
||||
// remove the symbol after the cursor
|
||||
line = append(line[:index], line[index+1:]...)
|
||||
// print the updated line
|
||||
for _, char := range line[index:] {
|
||||
EraseLine(rr.stdio.Out, ERASE_LINE_END)
|
||||
// print out the character
|
||||
if err := rr.printChar(char, mask); err != nil {
|
||||
return line, err
|
||||
}
|
||||
}
|
||||
// erase what's left on last line
|
||||
if cursorCurrent.Y < terminalSize.Y {
|
||||
cursor.NextLine(1)
|
||||
EraseLine(rr.stdio.Out, ERASE_LINE_END)
|
||||
}
|
||||
// restore cursor
|
||||
cursor.Restore()
|
||||
if len(line) == 0 || index == len(line) {
|
||||
EraseLine(rr.stdio.Out, ERASE_LINE_END)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// if the letter is another escape sequence
|
||||
if unicode.IsControl(r) || r == IgnoreKey {
|
||||
// ignore it
|
||||
continue
|
||||
}
|
||||
|
||||
// the user pressed a regular key
|
||||
|
||||
// if we are at the end of the line
|
||||
if index == len(line) {
|
||||
// just append the character at the end of the line
|
||||
line = append(line, r)
|
||||
// save the location of the cursor
|
||||
index++
|
||||
increment()
|
||||
// print out the character
|
||||
if err := rr.printChar(r, mask); err != nil {
|
||||
return line, err
|
||||
}
|
||||
} else {
|
||||
// we are in the middle of the word so we need to insert the character the user pressed
|
||||
line = append(line[:index], append([]rune{r}, line[index:]...)...)
|
||||
// save the current position of the cursor, as we have to move the cursor back to erase the current symbol
|
||||
// and then move for each symbol in line[index:] to print it out, afterwards we want to restore
|
||||
// cursor's location to its previous one.
|
||||
cursor.Save()
|
||||
EraseLine(rr.stdio.Out, ERASE_LINE_END)
|
||||
// remove the symbol after the cursor
|
||||
// print the updated line
|
||||
for _, char := range line[index:] {
|
||||
EraseLine(rr.stdio.Out, ERASE_LINE_END)
|
||||
// print out the character
|
||||
if err := rr.printChar(char, mask); err != nil {
|
||||
return line, err
|
||||
}
|
||||
increment()
|
||||
}
|
||||
// if we are at the last line, we want to visually insert a new line and append to it.
|
||||
if cursorCurrent.CursorIsAtLineEnd(terminalSize) && cursorCurrent.Y == terminalSize.Y {
|
||||
// add a new line to the terminal
|
||||
if _, err := fmt.Fprintln(rr.stdio.Out); err != nil {
|
||||
return line, err
|
||||
}
|
||||
// restore the position of the cursor horizontally
|
||||
cursor.Restore()
|
||||
// restore the position of the cursor vertically
|
||||
cursor.PreviousLine(1)
|
||||
} else {
|
||||
// restore cursor
|
||||
cursor.Restore()
|
||||
}
|
||||
// check if cursor needs to move to next line
|
||||
cursorCurrent, _ = cursor.Location(rr.Buffer())
|
||||
if cursorCurrent.CursorIsAtLineEnd(terminalSize) {
|
||||
cursor.NextLine(1)
|
||||
} else {
|
||||
cursor.Forward(runeWidth(r))
|
||||
}
|
||||
// increment the index
|
||||
index++
|
||||
increment()
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// runeWidth returns the number of columns spanned by a rune when printed to the terminal
|
||||
func runeWidth(r rune) int {
|
||||
switch width.LookupRune(r).Kind() {
|
||||
case width.EastAsianWide, width.EastAsianFullwidth:
|
||||
return 2
|
||||
}
|
||||
|
||||
if !unicode.IsPrint(r) {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
// isAnsiMarker returns if a rune denotes the start of an ANSI sequence
|
||||
func isAnsiMarker(r rune) bool {
|
||||
return r == '\x1B'
|
||||
}
|
||||
|
||||
// isAnsiTerminator returns if a rune denotes the end of an ANSI sequence
|
||||
func isAnsiTerminator(r rune) bool {
|
||||
return (r >= 0x40 && r <= 0x5a) || (r == 0x5e) || (r >= 0x60 && r <= 0x7e)
|
||||
}
|
||||
|
||||
// StringWidth returns the visible width of a string when printed to the terminal
|
||||
func StringWidth(str string) int {
|
||||
w := 0
|
||||
ansi := false
|
||||
|
||||
for _, r := range str {
|
||||
// increase width only when outside of ANSI escape sequences
|
||||
if ansi || isAnsiMarker(r) {
|
||||
ansi = !isAnsiTerminator(r)
|
||||
} else {
|
||||
w += runeWidth(r)
|
||||
}
|
||||
}
|
||||
return w
|
||||
}
|
||||
Loaded 100 of 240 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user