Compare commits
45 Commits
int-test-p
...
weblate-fi
Author | SHA1 | Date | |
---|---|---|---|
f282cadccd | |||
932c50ede3 | |||
cdf3f7d638 | |||
57b4b8e509 | |||
092ae6e700 | |||
436a938373 | |||
41cb05ccbd | |||
f53911f442 | |||
05788e44e0 | |||
e4e5f4f1f6 | |||
937656a9fa | |||
274c3a64c9 | |||
d4d0ddf74a | |||
bc948988a1 | |||
3a229d9dc9 | |||
d44b18d7be
|
|||
855a4c37c4
|
|||
7c3b740e14 | |||
2fbef41a3a
|
|||
6fb41e5300 | |||
1432f480c7 | |||
83af39771b
|
|||
4d1333202e
|
|||
55c24f070c
|
|||
229e8eb9da | |||
b3ab95750e
|
|||
de009921a2 | |||
d081bbaefa
|
|||
515b5466ca
|
|||
6965799bdc
|
|||
f75c9a6259
|
|||
a43a092ba7 | |||
fa084a61d2 | |||
895a7fe7d6
|
|||
742a726778
|
|||
2b9a185aff
|
|||
b7c1e87c0b
|
|||
cdfb8a08bb
|
|||
8943cea13f
|
|||
6d64e0edd3
|
|||
47045ca8f1 | |||
d0f982456e | |||
80ad6c6681 | |||
cb63cfe9c2
|
|||
d1e49d17ce
|
30
.drone.yml
30
.drone.yml
@ -18,6 +18,32 @@ steps:
|
||||
depends_on:
|
||||
- make check
|
||||
|
||||
- name: find updated translatable strings
|
||||
image: git.coopcloud.tech/toolshed/drone-xgotext
|
||||
depends_on:
|
||||
- make check
|
||||
settings:
|
||||
exclude: "vendor,.git,.bats"
|
||||
when:
|
||||
event: [push]
|
||||
|
||||
- name: commit catalogue template changes
|
||||
image: debian:bookworm
|
||||
environment:
|
||||
SSH_KEY:
|
||||
from_secret: abra_bot_deploy_key
|
||||
GIT_SSH_COMMAND: "ssh -o 'PubkeyAcceptedKeyTypes +ssh-rsa'"
|
||||
commands:
|
||||
- apt update && DEBIAN_FRONTEND=noninteractive apt install -y git openssh-client
|
||||
- mkdir $HOME/.ssh/
|
||||
- eval `ssh-agent`
|
||||
- echo "$SSH_KEY" | ssh-add -
|
||||
- ssh-keyscan -p 2222 -t rsa git.coopcloud.tech >> $HOME/.ssh/known_hosts
|
||||
- chmod -R go-rwx $HOME/.ssh
|
||||
- "git commit -a -m 'Chore: regenerate gettext catalogue template' -m '[ci skip]' && git push -u origin $DRONE_COMMIT_BRANCH"
|
||||
depends_on:
|
||||
- find updated translatable strings
|
||||
|
||||
- name: fetch
|
||||
image: docker:git
|
||||
commands:
|
||||
@ -26,7 +52,7 @@ steps:
|
||||
- make check
|
||||
- make test
|
||||
when:
|
||||
event: tag
|
||||
event: [tag]
|
||||
|
||||
- name: release
|
||||
image: goreleaser/goreleaser:v2.5.1
|
||||
@ -79,7 +105,7 @@ steps:
|
||||
sh run-ci-int
|
||||
when:
|
||||
ref:
|
||||
- int-*
|
||||
- refs/heads/int-*
|
||||
depends_on:
|
||||
- make check
|
||||
- make test
|
||||
|
@ -1,11 +1,12 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var AppCommand = &cobra.Command{
|
||||
Use: "app [cmd] [args] [flags]",
|
||||
Aliases: []string{"a"},
|
||||
Short: "Manage apps",
|
||||
Short: gotext.Get("Manage apps"),
|
||||
}
|
||||
|
@ -183,7 +183,7 @@ does not).`,
|
||||
if err := internal.RunCmdRemote(
|
||||
cl,
|
||||
app,
|
||||
requestTTY,
|
||||
disableTTY,
|
||||
app.Recipe.AbraShPath,
|
||||
targetServiceName, cmdName, parsedCmdArgs, remoteUser); err != nil {
|
||||
log.Fatal(err)
|
||||
@ -238,7 +238,7 @@ func parseCmdArgs(args []string, isLocal bool) (bool, string) {
|
||||
var (
|
||||
local bool
|
||||
remoteUser string
|
||||
requestTTY bool
|
||||
disableTTY bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -259,11 +259,11 @@ func init() {
|
||||
)
|
||||
|
||||
AppCmdCommand.Flags().BoolVarP(
|
||||
&requestTTY,
|
||||
&disableTTY,
|
||||
"tty",
|
||||
"T",
|
||||
false,
|
||||
"request remote TTY",
|
||||
"disable remote TTY",
|
||||
)
|
||||
|
||||
AppCmdCommand.Flags().BoolVarP(
|
||||
|
@ -33,7 +33,7 @@ var AppCpCommand = &cobra.Command{
|
||||
abra app cp 1312.net myfile.txt app:/
|
||||
|
||||
# copy that file back to your current working directory locally
|
||||
abra app cp 1312.net app:/myfile.txt`,
|
||||
abra app cp 1312.net app:/myfile.txt ./`,
|
||||
Args: cobra.ExactArgs(3),
|
||||
ValidArgsFunction: func(
|
||||
cmd *cobra.Command,
|
||||
|
@ -197,7 +197,25 @@ checkout as-is. Recipe commit hashes are also supported as values for
|
||||
|
||||
log.Debugf("set waiting timeout to %d second(s)", stack.WaitTimeout)
|
||||
|
||||
if err := stack.RunDeploy(cl, deployOpts, compose, app.Name, internal.DontWaitConverge); err != nil {
|
||||
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
f, err := app.Filters(true, false, serviceNames...)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := stack.RunDeploy(
|
||||
cl,
|
||||
deployOpts,
|
||||
compose,
|
||||
app.Name,
|
||||
app.Server,
|
||||
internal.DontWaitConverge,
|
||||
f,
|
||||
); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"coopcloud.tech/tagcmp"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -41,7 +42,7 @@ type serverStatus struct {
|
||||
var AppListCommand = &cobra.Command{
|
||||
Use: "list [flags]",
|
||||
Aliases: []string{"ls"},
|
||||
Short: "List all managed apps",
|
||||
Short: gotext.Get("List all managed apps"),
|
||||
Long: `Generate a report of all managed apps.
|
||||
|
||||
Use "--status/-S" flag to query all servers for the live deployment status.`,
|
||||
@ -142,10 +143,14 @@ Use "--status/-S" flag to query all servers for the live deployment status.`,
|
||||
appStats.AutoUpdate = autoUpdate
|
||||
|
||||
var newUpdates []string
|
||||
if version != "unknown" {
|
||||
if version != "unknown" && chaosVersion == "unknown" {
|
||||
if err := app.Recipe.EnsureExists(); err != nil {
|
||||
log.Fatalf("unable to clone %s: %s", app.Name, err)
|
||||
}
|
||||
|
||||
updates, err := app.Recipe.Tags()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("unable to retrieve tags for %s: %s", app.Name, err)
|
||||
}
|
||||
|
||||
parsedVersion, err := tagcmp.Parse(version)
|
||||
|
@ -3,23 +3,14 @@ package app
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
appPkg "coopcloud.tech/abra/pkg/app"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"coopcloud.tech/abra/pkg/logs"
|
||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
||||
"github.com/docker/docker/api/types"
|
||||
containerTypes "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -73,80 +64,25 @@ var AppLogsCommand = &cobra.Command{
|
||||
serviceNames = []string{args[1]}
|
||||
}
|
||||
|
||||
if err = tailLogs(cl, app, serviceNames); err != nil {
|
||||
f, err := app.Filters(true, false, serviceNames...)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
opts := logs.TailOpts{
|
||||
AppName: app.Name,
|
||||
Services: serviceNames,
|
||||
StdErr: stdErr,
|
||||
Since: sinceLogs,
|
||||
Filters: f,
|
||||
}
|
||||
|
||||
if err := logs.TailLogs(cl, opts); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// tailLogs prints logs for the given app with optional service names to be
|
||||
// filtered on. It also checks if the latest task is not runnning and then
|
||||
// prints the past tasks.
|
||||
func tailLogs(cl *dockerClient.Client, app appPkg.App, serviceNames []string) error {
|
||||
f, err := app.Filters(true, false, serviceNames...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
services, err := cl.ServiceList(context.Background(), types.ServiceListOptions{Filters: f})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, service := range services {
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("name", service.Spec.Name)
|
||||
tasks, err := cl.TaskList(context.Background(), types.TaskListOptions{Filters: f})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(tasks) > 0 {
|
||||
// Need to sort the tasks by the CreatedAt field in the inverse order.
|
||||
// Otherwise they are in the reversed order and not sorted properly.
|
||||
slices.SortFunc[[]swarm.Task](tasks, func(t1, t2 swarm.Task) int {
|
||||
return int(t2.Meta.CreatedAt.Unix() - t1.Meta.CreatedAt.Unix())
|
||||
})
|
||||
lastTask := tasks[0].Status
|
||||
if lastTask.State != swarm.TaskStateRunning {
|
||||
for _, task := range tasks {
|
||||
log.Errorf("[%s] %s State %s: %s", service.Spec.Name, task.Meta.CreatedAt.Format(time.RFC3339), task.Status.State, task.Status.Err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect the logs in a go routine, so the logs from all services are
|
||||
// collected in parallel.
|
||||
wg.Add(1)
|
||||
go func(serviceID string) {
|
||||
logs, err := cl.ServiceLogs(context.Background(), serviceID, containerTypes.LogsOptions{
|
||||
ShowStderr: true,
|
||||
ShowStdout: !stdErr,
|
||||
Since: sinceLogs,
|
||||
Until: "",
|
||||
Timestamps: true,
|
||||
Follow: true,
|
||||
Tail: "20",
|
||||
Details: false,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer logs.Close()
|
||||
|
||||
_, err = io.Copy(os.Stdout, logs)
|
||||
if err != nil && err != io.EOF {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}(service.ID)
|
||||
}
|
||||
|
||||
// Wait for all log streams to be closed.
|
||||
wg.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
stdErr bool
|
||||
sinceLogs string
|
||||
|
@ -109,6 +109,15 @@ var AppNewCommand = &cobra.Command{
|
||||
if err := recipe.EnsureLatest(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if recipeVersion == "" {
|
||||
head, err := recipe.Head()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to retrieve latest commit for %s: %s", recipe.Name, err)
|
||||
}
|
||||
|
||||
recipeVersion = formatter.SmallSHA(head.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -185,7 +194,7 @@ var AppNewCommand = &cobra.Command{
|
||||
newAppServer = "local"
|
||||
}
|
||||
|
||||
log.Infof("%s created successfully (version: %s, chaos: %s)", appDomain, recipeVersion, chaosVersion)
|
||||
log.Infof("%s created (version: %s)", appDomain, recipeVersion)
|
||||
|
||||
if len(appSecrets) > 0 {
|
||||
rows := [][]string{}
|
||||
@ -293,6 +302,12 @@ func ensureServerFlag() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(servers) == 1 {
|
||||
newAppServer = servers[0]
|
||||
log.Infof("single server detected, choosing %s automatically", newAppServer)
|
||||
return nil
|
||||
}
|
||||
|
||||
if newAppServer == "" && !internal.NoInput {
|
||||
prompt := &survey.Select{
|
||||
Message: "Select app server:",
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
@ -20,12 +21,14 @@ import (
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
)
|
||||
|
||||
var AppPsCommand = &cobra.Command{
|
||||
Use: "ps <domain> [flags]",
|
||||
Aliases: []string{"p"},
|
||||
Short: "Check app deployment status",
|
||||
Short: gotext.Get("Check app deployment status"),
|
||||
Args: cobra.ExactArgs(1),
|
||||
ValidArgsFunction: func(
|
||||
cmd *cobra.Command,
|
||||
@ -91,9 +94,14 @@ func showPSOutput(app appPkg.App, cl *dockerClient.Client, deployedVersion, chao
|
||||
return
|
||||
}
|
||||
|
||||
services := compose.Services
|
||||
sort.Slice(services, func(i, j int) bool {
|
||||
return services[i].Name < services[j].Name
|
||||
})
|
||||
|
||||
var rows [][]string
|
||||
allContainerStats := make(map[string]map[string]string)
|
||||
for _, service := range compose.Services {
|
||||
for _, service := range services {
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), service.Name))
|
||||
|
||||
|
@ -9,8 +9,10 @@ import (
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"coopcloud.tech/abra/pkg/ui"
|
||||
upstream "coopcloud.tech/abra/pkg/upstream/service"
|
||||
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -93,13 +95,36 @@ Pass "--all-services/-a" to restart all services.`,
|
||||
for _, serviceName := range serviceNames {
|
||||
stackServiceName := fmt.Sprintf("%s_%s", app.StackName(), serviceName)
|
||||
|
||||
service, _, err := cl.ServiceInspectWithRaw(
|
||||
context.Background(),
|
||||
stackServiceName,
|
||||
types.ServiceInspectOptions{},
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Debugf("attempting to scale %s to 0", stackServiceName)
|
||||
|
||||
if err := upstream.RunServiceScale(context.Background(), cl, stackServiceName, 0); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := stack.WaitOnService(context.Background(), cl, stackServiceName, app.Name); err != nil {
|
||||
f, err := app.Filters(true, false, serviceName)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
waitOpts := stack.WaitOpts{
|
||||
Services: []ui.ServiceMeta{{Name: stackServiceName, ID: service.ID}},
|
||||
AppName: app.Name,
|
||||
ServerName: app.Server,
|
||||
Filters: f,
|
||||
NoLog: true,
|
||||
Quiet: true,
|
||||
}
|
||||
|
||||
if err := stack.WaitOnServices(cmd.Context(), cl, waitOpts); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@ -110,7 +135,7 @@ Pass "--all-services/-a" to restart all services.`,
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := stack.WaitOnService(context.Background(), cl, stackServiceName, app.Name); err != nil {
|
||||
if err := stack.WaitOnServices(cmd.Context(), cl, waitOpts); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
|
@ -194,7 +194,32 @@ beforehand. See "abra app backup" for more.`,
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := stack.RunDeploy(cl, deployOpts, compose, stackName, internal.DontWaitConverge); err != nil {
|
||||
stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, stackName)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Debugf("set waiting timeout to %d second(s)", stack.WaitTimeout)
|
||||
|
||||
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
f, err := app.Filters(true, false, serviceNames...)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := stack.RunDeploy(
|
||||
cl,
|
||||
deployOpts,
|
||||
compose,
|
||||
stackName,
|
||||
app.Server,
|
||||
internal.DontWaitConverge,
|
||||
f,
|
||||
); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
|
@ -64,6 +64,24 @@ Passing "--prune/-p" does not remove those volumes.`,
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
opts := stack.Deploy{Composefiles: composeFiles, Namespace: stackName}
|
||||
compose, err := appPkg.GetAppComposeConfig(app.Name, opts, app.Env)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, stackName)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Info("initialising undeploy")
|
||||
|
||||
rmOpts := stack.Remove{
|
||||
Namespaces: []string{stackName},
|
||||
Detach: false,
|
||||
@ -78,6 +96,8 @@ Passing "--prune/-p" does not remove those volumes.`,
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("undeploy succeeded 🟢")
|
||||
|
||||
if err := app.WriteRecipeVersion(deployMeta.Version, false); err != nil {
|
||||
log.Fatalf("writing recipe version failed: %s", err)
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package app
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/app"
|
||||
@ -14,6 +15,7 @@ import (
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
"coopcloud.tech/abra/pkg/lint"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
stack "coopcloud.tech/abra/pkg/upstream/stack"
|
||||
"coopcloud.tech/tagcmp"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
@ -69,7 +71,13 @@ beforehand. See "abra app backup" for more.`,
|
||||
|
||||
app := internal.ValidateApp(args)
|
||||
|
||||
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||
if err := app.Recipe.Ensure(recipe.EnsureContext{
|
||||
Chaos: internal.Chaos,
|
||||
Offline: internal.Offline,
|
||||
// Ignore the env version for now, to make sure we are at the latest commit.
|
||||
// This enables us to get release notes, that were added after a release.
|
||||
IgnoreEnvVersion: true,
|
||||
}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@ -142,10 +150,9 @@ beforehand. See "abra app backup" for more.`,
|
||||
|
||||
log.Debugf("choosing %s as version to upgrade", chosenUpgrade)
|
||||
|
||||
// NOTE(d1): if release notes written after git tag published, read them
|
||||
// before we check out the tag and then they'll appear to be missing. this
|
||||
// covers when we obviously will forget to write release notes before
|
||||
// publishing
|
||||
// Get the release notes before checking out the new version in the
|
||||
// recipe. This enables us to get release notes, that were added after
|
||||
// a release.
|
||||
if err := getReleaseNotes(app, versions, chosenUpgrade, deployMeta, &upgradeReleaseNotes); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@ -207,9 +214,7 @@ beforehand. See "abra app backup" for more.`,
|
||||
return
|
||||
}
|
||||
|
||||
if upgradeReleaseNotes != "" && chosenUpgrade != "" {
|
||||
fmt.Print(upgradeReleaseNotes)
|
||||
} else {
|
||||
if upgradeReleaseNotes == "" {
|
||||
upgradeWarnMessages = append(
|
||||
upgradeWarnMessages,
|
||||
fmt.Sprintf("no release notes available for %s", chosenUpgrade),
|
||||
@ -233,7 +238,25 @@ beforehand. See "abra app backup" for more.`,
|
||||
|
||||
log.Debugf("set waiting timeout to %d second(s)", stack.WaitTimeout)
|
||||
|
||||
if err := stack.RunDeploy(cl, deployOpts, compose, stackName, internal.DontWaitConverge); err != nil {
|
||||
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
f, err := app.Filters(true, false, serviceNames...)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := stack.RunDeploy(
|
||||
cl,
|
||||
deployOpts,
|
||||
compose,
|
||||
stackName,
|
||||
app.Server,
|
||||
internal.DontWaitConverge,
|
||||
f,
|
||||
); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@ -313,6 +336,11 @@ func getReleaseNotes(
|
||||
}
|
||||
|
||||
if note != "" {
|
||||
// NOTE(d1): trim any final newline on the end of the note itself before
|
||||
// we manually handle newlines (for multiple release notes and
|
||||
// ensuring space between the warning messages)
|
||||
note = strings.TrimSuffix(note, "\n")
|
||||
|
||||
*upgradeReleaseNotes += fmt.Sprintf("%s\n", note)
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ import (
|
||||
func RunCmdRemote(
|
||||
cl *dockerClient.Client,
|
||||
app appPkg.App,
|
||||
requestTTY bool,
|
||||
disableTTY bool,
|
||||
abraSh, serviceName, cmdName, cmdArgs, remoteUser string) error {
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), serviceName))
|
||||
@ -84,8 +84,10 @@ func RunCmdRemote(
|
||||
}
|
||||
|
||||
execCreateOpts.Cmd = cmd
|
||||
execCreateOpts.Tty = requestTTY
|
||||
if !requestTTY {
|
||||
|
||||
execCreateOpts.Tty = true
|
||||
if disableTTY {
|
||||
execCreateOpts.Tty = false
|
||||
log.Debugf("not requesting a remote TTY")
|
||||
}
|
||||
|
||||
|
@ -46,7 +46,7 @@ func DeployOverview(
|
||||
app appPkg.App,
|
||||
deployedVersion string,
|
||||
toDeployVersion string,
|
||||
info string,
|
||||
releaseNotes string,
|
||||
warnMessages []string,
|
||||
) error {
|
||||
deployConfig := "compose.yml"
|
||||
@ -85,8 +85,8 @@ func DeployOverview(
|
||||
|
||||
fmt.Println(overview)
|
||||
|
||||
if info != "" {
|
||||
fmt.Println(info)
|
||||
if releaseNotes != "" {
|
||||
fmt.Print(releaseNotes)
|
||||
}
|
||||
|
||||
for _, msg := range warnMessages {
|
||||
|
@ -1,11 +1,15 @@
|
||||
package recipe
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
"github.com/go-git/go-git/v5"
|
||||
gitCfg "github.com/go-git/go-git/v5/config"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -13,7 +17,16 @@ var RecipeFetchCommand = &cobra.Command{
|
||||
Use: "fetch [recipe | --all] [flags]",
|
||||
Aliases: []string{"f"},
|
||||
Short: "Clone recipe(s) locally",
|
||||
Long: `Using "--force/-f" Git syncs an existing recipe. It does not erase unstaged changes.`,
|
||||
Args: cobra.RangeArgs(0, 1),
|
||||
Example: ` # fetch from recipe catalogue
|
||||
abra recipe fetch gitea
|
||||
|
||||
# fetch from remote recipe
|
||||
abra recipe fetch git.foo.org/recipes/myrecipe
|
||||
|
||||
# fetch with ssh remote for hacking
|
||||
abra recipe fetch gitea --ssh`,
|
||||
ValidArgsFunction: func(
|
||||
cmd *cobra.Command,
|
||||
args []string,
|
||||
@ -36,10 +49,39 @@ var RecipeFetchCommand = &cobra.Command{
|
||||
|
||||
ensureCtx := internal.GetEnsureContext()
|
||||
if recipeName != "" {
|
||||
r := internal.ValidateRecipe(args, cmd.Name())
|
||||
if err := r.Ensure(ensureCtx); err != nil {
|
||||
log.Fatal(err)
|
||||
r := recipe.Get(recipeName)
|
||||
if _, err := os.Stat(r.Dir); !os.IsNotExist(err) {
|
||||
if !force {
|
||||
log.Warnf("%s is already fetched", r.Name)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
r = internal.ValidateRecipe(args, cmd.Name())
|
||||
|
||||
if sshRemote {
|
||||
if r.SSHURL == "" {
|
||||
log.Warnf("unable to discover SSH remote for %s", r.Name)
|
||||
return
|
||||
}
|
||||
|
||||
repo, err := git.PlainOpen(r.Dir)
|
||||
if err != nil {
|
||||
log.Fatalf("unable to open %s: %s", r.Dir, err)
|
||||
}
|
||||
|
||||
if err = repo.DeleteRemote("origin"); err != nil {
|
||||
log.Fatalf("unable to remove default remote in %s: %s", r.Dir, err)
|
||||
}
|
||||
|
||||
if _, err := repo.CreateRemote(&gitCfg.RemoteConfig{
|
||||
Name: "origin",
|
||||
URLs: []string{r.SSHURL},
|
||||
}); err != nil {
|
||||
log.Fatalf("unable to set SSH remote in %s: %s", r.Dir, err)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@ -61,6 +103,8 @@ var RecipeFetchCommand = &cobra.Command{
|
||||
|
||||
var (
|
||||
fetchAllRecipes bool
|
||||
sshRemote bool
|
||||
force bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -71,4 +115,20 @@ func init() {
|
||||
false,
|
||||
"fetch all recipes",
|
||||
)
|
||||
|
||||
RecipeFetchCommand.Flags().BoolVarP(
|
||||
&sshRemote,
|
||||
"ssh",
|
||||
"s",
|
||||
false,
|
||||
"automatically set ssh remote",
|
||||
)
|
||||
|
||||
RecipeFetchCommand.Flags().BoolVarP(
|
||||
&force,
|
||||
"force",
|
||||
"f",
|
||||
false,
|
||||
"force re-fetch",
|
||||
)
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ func Run(version, commit string) {
|
||||
config.ABRA_DIR,
|
||||
config.SERVERS_DIR,
|
||||
config.RECIPES_DIR,
|
||||
config.LOGS_DIR,
|
||||
config.VENDOR_DIR, // TODO(d1): remove > 0.9.x
|
||||
config.BACKUP_DIR, // TODO(d1): remove > 0.9.x
|
||||
}
|
||||
|
@ -103,8 +103,7 @@ developer machine. The domain is then set to "default".`,
|
||||
|
||||
if _, err := client.New(name, timeout); err != nil {
|
||||
cleanUp(name)
|
||||
log.Debugf("ssh %s error: %s", name, sshPkg.Fatal(name, err))
|
||||
log.Fatalf("can't ssh to %s, make sure \"ssh %s\" works", name, name)
|
||||
log.Fatalf("ssh %s error: %s", name, sshPkg.Fatal(name, err))
|
||||
}
|
||||
|
||||
if created {
|
||||
|
@ -441,7 +441,25 @@ func upgrade(cl *dockerclient.Client, stackName, recipeName, upgradeVersion stri
|
||||
|
||||
log.Infof("upgrade %s (%s) to version %s", stackName, recipeName, upgradeVersion)
|
||||
|
||||
err = stack.RunDeploy(cl, deployOpts, compose, stackName, true)
|
||||
serviceNames, err := appPkg.GetAppServiceNames(app.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := app.Filters(true, false, serviceNames...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = stack.RunDeploy(
|
||||
cl,
|
||||
deployOpts,
|
||||
compose,
|
||||
stackName,
|
||||
app.Server,
|
||||
true,
|
||||
f,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
13
go.mod
13
go.mod
@ -1,13 +1,14 @@
|
||||
module coopcloud.tech/abra
|
||||
|
||||
go 1.23.0
|
||||
go 1.23.5
|
||||
|
||||
toolchain go1.23.1
|
||||
toolchain go1.24.1
|
||||
|
||||
require (
|
||||
coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb
|
||||
git.coopcloud.tech/toolshed/godotenv v1.5.2-0.20250103171850-4d0ca41daa5c
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
github.com/charmbracelet/bubbletea v1.3.4
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/charmbracelet/log v0.4.1
|
||||
github.com/distribution/reference v0.6.0
|
||||
@ -16,6 +17,7 @@ require (
|
||||
github.com/docker/go-units v0.5.0
|
||||
github.com/go-git/go-git/v5 v5.14.0
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/leonelquinteros/gotext v1.7.2
|
||||
github.com/moby/sys/signal v0.7.1
|
||||
github.com/moby/term v0.5.2
|
||||
github.com/pkg/errors v0.9.1
|
||||
@ -51,6 +53,7 @@ require (
|
||||
github.com/docker/go-metrics v0.0.1 // indirect
|
||||
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
@ -73,17 +76,19 @@ require (
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mmcloughlin/avo v0.6.0 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/sys/mountinfo v0.6.2 // indirect
|
||||
github.com/moby/sys/user v0.3.0 // indirect
|
||||
github.com/moby/sys/userns v0.1.0 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
@ -118,12 +123,10 @@ require (
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/net v0.37.0 // indirect
|
||||
golang.org/x/sync v0.12.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
golang.org/x/tools v0.31.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 // indirect
|
||||
google.golang.org/grpc v1.71.0 // indirect
|
||||
|
152
go.sum
152
go.sum
@ -79,8 +79,6 @@ github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb0
|
||||
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/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk=
|
||||
github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
|
||||
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
@ -133,23 +131,18 @@ github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInq
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI=
|
||||
github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||
github.com/charmbracelet/lipgloss v1.0.0 h1:O7VkGDvqEdGi93X+DeqsQ7PKHDgtQfF8j8/O2qFMQNg=
|
||||
github.com/charmbracelet/lipgloss v1.0.0/go.mod h1:U5fy9Z+C38obMs+T+tJqst9VGzlOYGj4ri9reL3qUlo=
|
||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
|
||||
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
|
||||
github.com/charmbracelet/log v0.4.1 h1:6AYnoHKADkghm/vt4neaNEXkxcXLSV2g1rdyFDOpTyk=
|
||||
github.com/charmbracelet/log v0.4.1/go.mod h1:pXgyTsqsVu4N9hGdHmQ0xEA4RsXof402LX9ZgiITn2I=
|
||||
github.com/charmbracelet/x/ansi v0.6.0 h1:qOznutrb93gx9oMiGf7caF7bqqubh6YIM0SWKyA08pA=
|
||||
github.com/charmbracelet/x/ansi v0.6.0/go.mod h1:KBUFw1la39nl0dLl10l5ORDAqGXaeurTQmwyyVKse/Q=
|
||||
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
||||
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
|
||||
@ -175,8 +168,6 @@ github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2u
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e85keuznYcH5rqI438v41pKcBl4ZxQ=
|
||||
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
|
||||
github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys=
|
||||
github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
|
||||
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
@ -292,7 +283,6 @@ github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfc
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
@ -302,8 +292,6 @@ github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
|
||||
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
|
||||
github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM=
|
||||
github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ=
|
||||
@ -324,8 +312,6 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
|
||||
github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v27.4.1+incompatible h1:VzPiUlRJ/xh+otB75gva3r05isHMo5wXDfPRi5/b4hI=
|
||||
github.com/docker/cli v27.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v28.0.1+incompatible h1:g0h5NQNda3/CxIsaZfH4Tyf6vpxFth7PYl3hgCPOKzs=
|
||||
github.com/docker/cli v28.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
|
||||
@ -334,13 +320,9 @@ github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4Kfc
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v27.4.1+incompatible h1:ZJvcY7gfwHn1JF48PfbyXg7Jyt9ZCWDW+GGXOIxEwp4=
|
||||
github.com/docker/docker v27.4.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v28.0.1+incompatible h1:FCHjSRdXhNRFjlHMTv4jUNlIBbTeRjrWfeFuJp7jpo0=
|
||||
github.com/docker/docker v28.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
|
||||
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
|
||||
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
|
||||
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
|
||||
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
|
||||
@ -365,9 +347,8 @@ github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:Htrtb
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM=
|
||||
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
||||
github.com/elazarl/goproxy v1.2.3 h1:xwIyKHbaP5yfT6O9KIeYJR5549MXRQkoQMRXGztz8YQ=
|
||||
github.com/elazarl/goproxy v1.2.3/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64=
|
||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
||||
github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
@ -378,6 +359,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
|
||||
github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
@ -402,14 +385,10 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.6.1 h1:u+dcrgaguSSkbjzHwelEjc0Yj300NUevrrPphk/SoRA=
|
||||
github.com/go-git/go-billy/v5 v5.6.1/go.mod h1:0AsLr1z2+Uksi4NlElmMblP5rPcDZNRCD8ujZCRR2BE=
|
||||
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
|
||||
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.13.1 h1:DAQ9APonnlvSWpvolXWIuV6Q6zXy2wHbN4cVlNR5Q+M=
|
||||
github.com/go-git/go-git/v5 v5.13.1/go.mod h1:qryJB4cSBoq3FRoBRf5A77joojuBcmPJ0qu3XXXVixc=
|
||||
github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60=
|
||||
github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
@ -508,8 +487,6 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0=
|
||||
@ -551,10 +528,7 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
|
||||
@ -618,8 +592,6 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
|
||||
github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.14.2/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
@ -639,6 +611,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leonelquinteros/gotext v1.7.2 h1:bDPndU8nt+/kRo1m4l/1OXiiy2v7Z7dfPQ9+YP7G1Mc=
|
||||
github.com/leonelquinteros/gotext v1.7.2/go.mod h1:9/haCkm5P7Jay1sxKDGJ5WIg4zkz8oZKw4ekNpALob8=
|
||||
github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
@ -653,15 +627,14 @@ github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7
|
||||
github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
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.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
@ -688,8 +661,6 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=
|
||||
github.com/mmcloughlin/avo v0.6.0 h1:QH6FU8SKoTLaVs80GA8TJuLNkUYl4VokHKlPhVDg4YY=
|
||||
github.com/mmcloughlin/avo v0.6.0/go.mod h1:8CoAGaCSYXtCPR+8y18Y9aB/kxb8JSS6FRI7mSkvD+8=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
|
||||
@ -710,8 +681,6 @@ github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85
|
||||
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
|
||||
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
|
||||
github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@ -722,8 +691,10 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
|
||||
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
||||
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
@ -762,8 +733,6 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
|
||||
@ -795,8 +764,6 @@ github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrap
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
||||
github.com/pjbgf/sha1cd v0.3.1 h1:Dh2GYdpJnO84lIw0LJwTFXjcNbasP/bklicSznyAaPI=
|
||||
github.com/pjbgf/sha1cd v0.3.1/go.mod h1:Y8t7jSB/dEI/lQE04A1HVKteqjj9bX5O4+Cex0TCu8s=
|
||||
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
|
||||
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@ -814,8 +781,6 @@ github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDf
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
|
||||
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
|
||||
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
@ -831,8 +796,6 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ=
|
||||
github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s=
|
||||
github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k=
|
||||
github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18=
|
||||
github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
@ -855,18 +818,13 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
|
||||
github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/schollz/progressbar/v3 v3.17.1 h1:bI1MTaoQO+v5kzklBjYNRQLoVpe0zbyRZNK6DFkVC5U=
|
||||
github.com/schollz/progressbar/v3 v3.17.1/go.mod h1:RzqpnsPQNjUyIgdglUjRLgD7sVnxN1wpmBMV+UiEbL4=
|
||||
github.com/schollz/progressbar/v3 v3.18.0 h1:uXdoHABRFmNIjUfte/Ex7WtuyVslrw2wVPQmCN62HpA=
|
||||
github.com/schollz/progressbar/v3 v3.18.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
|
||||
github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U=
|
||||
@ -885,8 +843,6 @@ github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic
|
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY=
|
||||
github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M=
|
||||
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
|
||||
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
@ -903,8 +859,6 @@ github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3
|
||||
github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/jwalterweatherman v0.0.0-20141219030609-3d60171a6431/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
@ -915,7 +869,6 @@ github.com/spf13/pflag v1.0.0/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
|
||||
github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
@ -995,47 +948,27 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
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/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
|
||||
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
|
||||
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0 h1:7F29RDmnlqk6B5d+sUqemt8TBfDqxryYW5gX6L74RFA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0/go.mod h1:ZiGDq7xwDMKmWDrN1XsXAj0iC7hns+2DhxBFSncNHSE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
|
||||
go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
|
||||
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
|
||||
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.33.0 h1:Gs5VK9/WUJhNXZgn8MR6ITatvAmKeIuCtNbsP3JkNqU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.33.0/go.mod h1:dL5ykHZmm1B1nVRk9dDjChwDmt81MjVp3gLkQRwKf/Q=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
|
||||
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg=
|
||||
go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
@ -1062,10 +995,6 @@ golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWP
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
@ -1078,10 +1007,6 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 h1:9kj3STMvgqy3YA4VQXBrN7925ICMxD5wzMRcgA30588=
|
||||
golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
|
||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
|
||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
@ -1106,10 +1031,6 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/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.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@ -1151,10 +1072,6 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
|
||||
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@ -1174,8 +1091,6 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@ -1247,6 +1162,7 @@ golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/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-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@ -1254,22 +1170,13 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
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.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@ -1281,8 +1188,6 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@ -1291,10 +1196,6 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@ -1342,12 +1243,6 @@ golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4X
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
|
||||
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
|
||||
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
||||
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
||||
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||
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=
|
||||
@ -1396,16 +1291,8 @@ google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfG
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d h1:H8tOf8XM88HvKqLTxe755haY6r1fqqzLbEnfrmLXlSA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250102185135-69823020774d/go.mod h1:2v7Z7gP2ZUOGsaFyxATQSRoBnKygqVq2Cwnvom7QiqY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4 h1:IFnXJq3UPB3oBREOodn1v1aGQeZYQclEmvWRMN0PSsY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:c8q6Z6OCqnfVIqUFJkCzKcrj8eCvUrz+K4KRzSTuANg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d h1:xJJRGY7TJcvIlpSrN3K6LAWgNFUILlO+OMAqtg9aqnw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 h1:3UsHvIr4Wc2aW4brOaSCmcxh9ksica6fHEr8P1XhkYw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4 h1:iK2jbkWL86DXjEx0qiHcRE9dE4/Ahua5k6V8OWFb//c=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250313205543-e70fdf4c4cb4/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
|
||||
google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
@ -1427,8 +1314,6 @@ google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTp
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
|
||||
google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU=
|
||||
google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
|
||||
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
|
||||
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
@ -1444,10 +1329,6 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
|
||||
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
|
||||
google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||
@ -1489,12 +1370,9 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
|
||||
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
|
||||
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
|
||||
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
20
locales/default.pot
Normal file
20
locales/default.pot
Normal file
@ -0,0 +1,20 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Language: \n"
|
||||
"X-Generator: xgotext\n"
|
||||
|
||||
#: cli/app/ps.go:31
|
||||
msgid "Check app deployment status"
|
||||
msgstr ""
|
||||
|
||||
#: cli/app/list.go:45
|
||||
msgid "List all managed apps"
|
||||
msgstr ""
|
||||
|
||||
#: cli/app/app.go:11
|
||||
msgid "Manage apps"
|
||||
msgstr ""
|
30
locales/es/LC_MESSAGES/default.po
Normal file
30
locales/es/LC_MESSAGES/default.po
Normal file
@ -0,0 +1,30 @@
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-08-04 14:15+0000\n"
|
||||
"PO-Revision-Date: 2025-08-04 14:15+0000\n"
|
||||
"Last-Translator: 3wordchant <3wc.coopcloud@doesthisthing.work>\n"
|
||||
"Language-Team: Spanish <https://translate.coopcloud.tech/projects/co-op-"
|
||||
"cloud/abra/es/>\n"
|
||||
"Language: es\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: ENCODING\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.12.2\n"
|
||||
|
||||
#: cli/app/ps.go:31
|
||||
msgid "Check app deployment status"
|
||||
msgstr ""
|
||||
|
||||
#: cli/app/list.go:45
|
||||
#, fuzzy
|
||||
#| msgid "Manage apps"
|
||||
msgid "List all managed apps"
|
||||
msgstr "Gestionar aplicaciones"
|
||||
|
||||
#: cli/app/app.go:11
|
||||
msgid "Manage apps"
|
||||
msgstr "Gestionar aplicaciones"
|
@ -90,6 +90,7 @@ func (a Abra) GetAbraDir() string {
|
||||
|
||||
func (a Abra) GetServersDir() string { return path.Join(a.GetAbraDir(), "servers") }
|
||||
func (a Abra) GetRecipesDir() string { return path.Join(a.GetAbraDir(), "recipes") }
|
||||
func (a Abra) GetLogsDir() string { return path.Join(a.GetAbraDir(), "logs") }
|
||||
func (a Abra) GetVendorDir() string { return path.Join(a.GetAbraDir(), "vendor") }
|
||||
func (a Abra) GetBackupDir() string { return path.Join(a.GetAbraDir(), "backups") }
|
||||
func (a Abra) GetCatalogueDir() string { return path.Join(a.GetAbraDir(), "catalogue") }
|
||||
@ -100,6 +101,7 @@ var (
|
||||
ABRA_DIR = config.GetAbraDir()
|
||||
SERVERS_DIR = config.GetServersDir()
|
||||
RECIPES_DIR = config.GetRecipesDir()
|
||||
LOGS_DIR = config.GetLogsDir()
|
||||
VENDOR_DIR = config.GetVendorDir()
|
||||
BACKUP_DIR = config.GetBackupDir()
|
||||
CATALOGUE_DIR = config.GetCatalogueDir()
|
||||
|
@ -1,7 +1,10 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
@ -22,12 +25,28 @@ func gitCloneIgnoreErr(err error) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Clone runs a git clone which accounts for different default branches.
|
||||
// Clone runs a git clone which accounts for different default branches. This
|
||||
// function respects Ctrl+C (SIGINT) calls from the user, cancelling the
|
||||
// context and deleting the (typically) half-baked clone of the repository.
|
||||
// This avoids broken state for future clone / recipe ops.
|
||||
func Clone(dir, url string) error {
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
log.Debugf("git clone: %s", dir, url)
|
||||
ctx := context.Background()
|
||||
ctx, cancelCtx := context.WithCancel(ctx)
|
||||
|
||||
_, err := git.PlainClone(dir, false, &git.CloneOptions{
|
||||
sigIntCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigIntCh, os.Interrupt)
|
||||
defer func() {
|
||||
signal.Stop(sigIntCh)
|
||||
cancelCtx()
|
||||
}()
|
||||
|
||||
errCh := make(chan error)
|
||||
|
||||
go func() {
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
log.Debugf("git clone: %s", url)
|
||||
|
||||
_, err := git.PlainCloneContext(ctx, dir, false, &git.CloneOptions{
|
||||
URL: url,
|
||||
Tags: git.AllTags,
|
||||
ReferenceName: plumbing.ReferenceName("refs/heads/main"),
|
||||
@ -36,13 +55,17 @@ func Clone(dir, url string) error {
|
||||
|
||||
if err != nil && gitCloneIgnoreErr(err) {
|
||||
log.Debugf("git clone: %s cloned successfully", dir)
|
||||
return nil
|
||||
errCh <- nil
|
||||
}
|
||||
|
||||
if err := ctx.Err(); err != nil {
|
||||
errCh <- fmt.Errorf("git clone %s: cancelled due to interrupt", dir)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Debug("git clone: main branch failed, attempting master branch")
|
||||
|
||||
_, err := git.PlainClone(dir, false, &git.CloneOptions{
|
||||
_, err := git.PlainCloneContext(ctx, dir, false, &git.CloneOptions{
|
||||
URL: url,
|
||||
Tags: git.AllTags,
|
||||
ReferenceName: plumbing.ReferenceName("refs/heads/master"),
|
||||
@ -51,11 +74,11 @@ func Clone(dir, url string) error {
|
||||
|
||||
if err != nil && gitCloneIgnoreErr(err) {
|
||||
log.Debugf("git clone: %s cloned successfully", dir)
|
||||
return nil
|
||||
errCh <- nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
errCh <- err
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,5 +87,20 @@ func Clone(dir, url string) error {
|
||||
log.Debugf("git clone: %s already exists", dir)
|
||||
}
|
||||
|
||||
errCh <- nil
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-sigIntCh:
|
||||
cancelCtx()
|
||||
fmt.Println() // NOTE(d1): newline after ^C
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
return fmt.Errorf("unable to clean up git clone of %s: %s", dir, err)
|
||||
}
|
||||
return fmt.Errorf("git clone %s: cancelled due to interrupt", dir)
|
||||
case err := <-errCh:
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
48
pkg/git/clone_test.go
Normal file
48
pkg/git/clone_test.go
Normal file
@ -0,0 +1,48 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
)
|
||||
|
||||
func TestClone(t *testing.T) {
|
||||
dir := path.Join(config.RECIPES_DIR, "gitea")
|
||||
os.RemoveAll(dir)
|
||||
|
||||
gitURL := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, "gitea")
|
||||
if err := Clone(dir, gitURL); err != nil {
|
||||
t.Fatalf("unable to git clone gitea: %s", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) {
|
||||
t.Fatal("gitea repo was not cloned successfully")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelGitClone(t *testing.T) {
|
||||
dir := path.Join(config.RECIPES_DIR, "gitea")
|
||||
os.RemoveAll(dir)
|
||||
|
||||
go func() {
|
||||
p, err := os.FindProcess(os.Getpid())
|
||||
if err != nil {
|
||||
t.Fatalf("unable to find current process: %s", err)
|
||||
}
|
||||
|
||||
p.Signal(syscall.SIGINT)
|
||||
}()
|
||||
|
||||
gitURL := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, "gitea")
|
||||
if err := Clone(dir, gitURL); err == nil {
|
||||
t.Fatal("cloning should have been interrupted")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(dir); err != nil && !os.IsNotExist(err) {
|
||||
t.Fatal("recipe repo was not deleted")
|
||||
}
|
||||
}
|
30
pkg/lang/lang.go
Normal file
30
pkg/lang/lang.go
Normal file
@ -0,0 +1,30 @@
|
||||
package lang
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GetLocale() string {
|
||||
if loc := os.Getenv("LC_MESSAGES"); loc != "" {
|
||||
return NormalizeLocale(loc)
|
||||
}
|
||||
|
||||
if loc := os.Getenv("LANG"); loc != "" {
|
||||
return NormalizeLocale(loc)
|
||||
}
|
||||
|
||||
return "C.UTF-8"
|
||||
}
|
||||
|
||||
func NormalizeLocale(loc string) string {
|
||||
if idx := strings.Index(loc, "."); idx != -1 {
|
||||
return loc[:idx]
|
||||
}
|
||||
|
||||
if idx := strings.Index(loc, "@"); idx != -1 {
|
||||
return loc[:idx]
|
||||
}
|
||||
|
||||
return loc
|
||||
}
|
@ -15,8 +15,10 @@ import (
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
)
|
||||
|
||||
var Warn = "warn"
|
||||
var Critical = "critical"
|
||||
var (
|
||||
Warn = "warn"
|
||||
Critical = "critical"
|
||||
)
|
||||
|
||||
type LintFunction func(recipe.Recipe) (bool, error)
|
||||
|
||||
@ -194,7 +196,7 @@ func LintForErrors(recipe recipe.Recipe) error {
|
||||
|
||||
ok, err := rule.Function(recipe)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("lint %s: %s", rule.Ref, err)
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("lint error in %s configs: \"%s\" failed lint checks (%s)", recipe.Name, rule.Description, rule.Ref)
|
||||
|
@ -2,8 +2,10 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"math"
|
||||
"os"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
charmLog "github.com/charmbracelet/log"
|
||||
)
|
||||
|
||||
@ -32,3 +34,13 @@ var SetLevel = Logger.SetLevel
|
||||
var DebugLevel = charmLog.DebugLevel
|
||||
var SetOutput = charmLog.SetOutput
|
||||
var SetReportCaller = charmLog.SetReportCaller
|
||||
|
||||
type f func() (tea.Model, error)
|
||||
|
||||
func Without(fn f) (tea.Model, error) {
|
||||
l := Logger.GetLevel()
|
||||
Logger.SetLevel(math.MaxInt)
|
||||
m, err := fn()
|
||||
Logger.SetLevel(l)
|
||||
return m, err
|
||||
}
|
||||
|
104
pkg/logs/logs.go
Normal file
104
pkg/logs/logs.go
Normal file
@ -0,0 +1,104 @@
|
||||
package logs
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
containerTypes "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
type TailOpts struct {
|
||||
AppName string
|
||||
Services []string
|
||||
StdErr bool
|
||||
Since string
|
||||
Buffer *[]string
|
||||
ToBuffer bool
|
||||
Filters filters.Args
|
||||
}
|
||||
|
||||
// TailLogs gathers logs for the given app with optional service names to be
|
||||
// filtered on. These logs can be printed to os.Stdout or gathered to a buffer.
|
||||
func TailLogs(
|
||||
cl *dockerClient.Client,
|
||||
opts TailOpts,
|
||||
) error {
|
||||
sigIntCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigIntCh, os.Interrupt)
|
||||
defer signal.Stop(sigIntCh)
|
||||
|
||||
services, err := cl.ServiceList(
|
||||
context.Background(),
|
||||
types.ServiceListOptions{Filters: opts.Filters},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
errCh := make(chan error)
|
||||
waitCh := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
var wg sync.WaitGroup
|
||||
for _, service := range services {
|
||||
wg.Add(1)
|
||||
go func(serviceID string) {
|
||||
tail := "50"
|
||||
if opts.ToBuffer {
|
||||
// NOTE(d1): more logs from before deployment when analysing via file
|
||||
tail = "150"
|
||||
}
|
||||
|
||||
logs, err := cl.ServiceLogs(context.Background(), serviceID, containerTypes.LogsOptions{
|
||||
ShowStderr: true,
|
||||
ShowStdout: !opts.StdErr,
|
||||
Since: opts.Since,
|
||||
Until: "",
|
||||
Timestamps: true,
|
||||
Follow: true,
|
||||
Tail: tail,
|
||||
Details: false,
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
defer logs.Close()
|
||||
if opts.ToBuffer {
|
||||
buf := bufio.NewScanner(logs)
|
||||
for buf.Scan() {
|
||||
line := fmt.Sprintf("%s: %s", service.Spec.Name, buf.Text())
|
||||
*opts.Buffer = append(*opts.Buffer, line)
|
||||
}
|
||||
logs.Close()
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = io.Copy(os.Stdout, logs); err != nil && err != io.EOF {
|
||||
errCh <- fmt.Errorf("tailLogs: unable to copy buffer: %s", err)
|
||||
}
|
||||
}
|
||||
}(service.ID)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(waitCh)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-waitCh:
|
||||
return nil
|
||||
case <-sigIntCh:
|
||||
return nil
|
||||
case err := <-errCh:
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -33,6 +33,10 @@ type Secret struct {
|
||||
// variable. For Example:
|
||||
// SECRET_FOO=v1 # length=12
|
||||
Length int
|
||||
// Charset comes from the charset modifier at the secret version environment
|
||||
// variable. For Example:
|
||||
// SECRET_FOO=v1 # charset=default,special
|
||||
Charset string
|
||||
// RemoteName is the name of the secret on the server. For example:
|
||||
// name: ${STACK_NAME}_test_pass_two_${SECRET_TEST_PASS_TWO_VERSION}
|
||||
// With the following:
|
||||
@ -43,38 +47,38 @@ type Secret struct {
|
||||
RemoteName string
|
||||
}
|
||||
|
||||
// GeneratePasswords generates passwords.
|
||||
func GeneratePasswords(count, length uint) ([]string, error) {
|
||||
// GeneratePassword generates passwords.
|
||||
func GeneratePassword(length uint, charset string) (string, error) {
|
||||
passwords, err := passgen.GeneratePasswords(
|
||||
count,
|
||||
1,
|
||||
length,
|
||||
passgen.AlphabetDefault,
|
||||
charset,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", err
|
||||
}
|
||||
|
||||
log.Debugf("generated %s", strings.Join(passwords, ", "))
|
||||
|
||||
return passwords, nil
|
||||
return passwords[0], nil
|
||||
}
|
||||
|
||||
// GeneratePassphrases generates human readable and rememberable passphrases.
|
||||
func GeneratePassphrases(count uint) ([]string, error) {
|
||||
// GeneratePassphrase generates human readable and rememberable passphrases.
|
||||
func GeneratePassphrase() (string, error) {
|
||||
passphrases, err := passgen.GeneratePassphrases(
|
||||
count,
|
||||
1,
|
||||
passgen.PassphraseWordCountDefault,
|
||||
rune('-'),
|
||||
passgen.PassphraseCasingDefault,
|
||||
passgen.WordListDefault,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", err
|
||||
}
|
||||
|
||||
log.Debugf("generated %s", strings.Join(passphrases, ", "))
|
||||
|
||||
return passphrases, nil
|
||||
return passphrases[0], nil
|
||||
}
|
||||
|
||||
// ReadSecretsConfig reads secret names/versions from the recipe config. The
|
||||
@ -150,6 +154,8 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin
|
||||
}
|
||||
value.Length = length
|
||||
}
|
||||
|
||||
value.Charset = resolveCharset(modifierValues["charset"])
|
||||
break
|
||||
}
|
||||
secretValues[secretId] = value
|
||||
@ -158,6 +164,22 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin
|
||||
return secretValues, nil
|
||||
}
|
||||
|
||||
// resolveCharset sets the passgen Alphabet required for a secret
|
||||
func resolveCharset(input string) string {
|
||||
switch strings.ToLower(input) {
|
||||
case "special":
|
||||
return passgen.AlphabetSpecial
|
||||
case "safespecial":
|
||||
return "!@#%^&*_-+="
|
||||
case "default,special", "special,default":
|
||||
return passgen.AlphabetDefault + passgen.AlphabetSpecial
|
||||
case "default,safespecial", "safespecial,default":
|
||||
return passgen.AlphabetDefault + "!@#%^&*_-+="
|
||||
default:
|
||||
return passgen.AlphabetDefault // Fallback to default
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateSecrets generates secrets locally and sends them to a remote server for storage.
|
||||
func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server string) (map[string]string, error) {
|
||||
secretsGenerated := map[string]string{}
|
||||
@ -173,13 +195,13 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
|
||||
log.Debugf("attempting to generate and store %s on %s", secret.RemoteName, server)
|
||||
|
||||
if secret.Length > 0 {
|
||||
passwords, err := GeneratePasswords(1, uint(secret.Length))
|
||||
password, err := GeneratePassword(uint(secret.Length), secret.Charset)
|
||||
if err != nil {
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.StoreSecret(cl, secret.RemoteName, passwords[0], server); err != nil {
|
||||
if err := client.StoreSecret(cl, secret.RemoteName, password, server); err != nil {
|
||||
if strings.Contains(err.Error(), "AlreadyExists") {
|
||||
log.Warnf("%s already exists", secret.RemoteName)
|
||||
ch <- nil
|
||||
@ -191,15 +213,15 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
|
||||
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
secretsGenerated[secretName] = passwords[0]
|
||||
secretsGenerated[secretName] = password
|
||||
} else {
|
||||
passphrases, err := GeneratePassphrases(1)
|
||||
passphrase, err := GeneratePassphrase()
|
||||
if err != nil {
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
|
||||
if err := client.StoreSecret(cl, secret.RemoteName, passphrases[0], server); err != nil {
|
||||
if err := client.StoreSecret(cl, secret.RemoteName, passphrase, server); err != nil {
|
||||
if strings.Contains(err.Error(), "AlreadyExists") {
|
||||
log.Warnf("%s already exists", secret.RemoteName)
|
||||
ch <- nil
|
||||
@ -211,7 +233,7 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
|
||||
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
secretsGenerated[secretName] = passphrases[0]
|
||||
secretsGenerated[secretName] = passphrase
|
||||
}
|
||||
ch <- nil
|
||||
}(n, v)
|
||||
|
@ -17,16 +17,37 @@ func TestReadSecretsConfig(t *testing.T) {
|
||||
assert.Equal(t, "test_example_com_test_pass_one_v2", secretsFromConfig["test_pass_one"].RemoteName)
|
||||
assert.Equal(t, "v2", secretsFromConfig["test_pass_one"].Version)
|
||||
assert.Equal(t, 0, secretsFromConfig["test_pass_one"].Length)
|
||||
assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789", secretsFromConfig["test_pass_one"].Charset)
|
||||
|
||||
// Has a length modifier
|
||||
assert.Equal(t, "test_example_com_test_pass_two_v1", secretsFromConfig["test_pass_two"].RemoteName)
|
||||
assert.Equal(t, "v1", secretsFromConfig["test_pass_two"].Version)
|
||||
assert.Equal(t, 10, secretsFromConfig["test_pass_two"].Length)
|
||||
assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789", secretsFromConfig["test_pass_two"].Charset)
|
||||
|
||||
// Secret name does not include the secret id
|
||||
assert.Equal(t, "test_example_com_pass_three_v2", secretsFromConfig["test_pass_three"].RemoteName)
|
||||
assert.Equal(t, "v2", secretsFromConfig["test_pass_three"].Version)
|
||||
assert.Equal(t, 0, secretsFromConfig["test_pass_three"].Length)
|
||||
assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789", secretsFromConfig["test_pass_three"].Charset)
|
||||
|
||||
// Has a length modifier and a charset=default,safespecial modifier
|
||||
assert.Equal(t, "test_example_com_test_pass_four_v1", secretsFromConfig["test_pass_four"].RemoteName)
|
||||
assert.Equal(t, "v1", secretsFromConfig["test_pass_four"].Version)
|
||||
assert.Equal(t, 12, secretsFromConfig["test_pass_four"].Length)
|
||||
assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#%^&*_-+=", secretsFromConfig["test_pass_four"].Charset)
|
||||
|
||||
// Has a length modifier and a charset=default,special modifier
|
||||
assert.Equal(t, "test_example_com_test_pass_five_v1", secretsFromConfig["test_pass_five"].RemoteName)
|
||||
assert.Equal(t, "v1", secretsFromConfig["test_pass_five"].Version)
|
||||
assert.Equal(t, 12, secretsFromConfig["test_pass_five"].Length)
|
||||
assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#$%^&*_-+=", secretsFromConfig["test_pass_five"].Charset)
|
||||
|
||||
// Has only a charset=default,special modifier, which gets setted but ignored in the generation
|
||||
assert.Equal(t, "test_example_com_test_pass_six_v1", secretsFromConfig["test_pass_six"].RemoteName)
|
||||
assert.Equal(t, "v1", secretsFromConfig["test_pass_six"].Version)
|
||||
assert.Equal(t, 0, secretsFromConfig["test_pass_six"].Length)
|
||||
assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789!@#$%^&*_-+=", secretsFromConfig["test_pass_six"].Charset)
|
||||
}
|
||||
|
||||
func TestReadSecretsConfigWithLongDomain(t *testing.T) {
|
||||
|
@ -1,3 +1,6 @@
|
||||
SECRET_TEST_PASS_ONE_VERSION=v2
|
||||
SECRET_TEST_PASS_TWO_VERSION=v1 # length=10
|
||||
SECRET_TEST_PASS_THREE_VERSION=v2
|
||||
SECRET_TEST_PASS_FOUR_VERSION=v1 # length=12 charset=default,safespecial
|
||||
SECRET_TEST_PASS_FIVE_VERSION=v1 # length=12 charset=default,special
|
||||
SECRET_TEST_PASS_SIX_VERSION=v1 # charset=default,special
|
||||
|
@ -8,6 +8,9 @@ services:
|
||||
- test_pass_one
|
||||
- test_pass_two
|
||||
- test_pass_three
|
||||
- test_pass_four
|
||||
- test_pass_five
|
||||
- test_pass_six
|
||||
|
||||
secrets:
|
||||
test_pass_one:
|
||||
@ -19,3 +22,12 @@ secrets:
|
||||
test_pass_three:
|
||||
external: true
|
||||
name: ${STACK_NAME}_pass_three_${SECRET_TEST_PASS_THREE_VERSION} # secretId and name don't match
|
||||
test_pass_four:
|
||||
external: true
|
||||
name: ${STACK_NAME}_test_pass_four_${SECRET_TEST_PASS_FOUR_VERSION}
|
||||
test_pass_five:
|
||||
external: true
|
||||
name: ${STACK_NAME}_test_pass_five_${SECRET_TEST_PASS_FIVE_VERSION}
|
||||
test_pass_six:
|
||||
external: true
|
||||
name: ${STACK_NAME}_test_pass_six_${SECRET_TEST_PASS_SIX_VERSION}
|
||||
|
@ -20,6 +20,8 @@ func Fatal(hostname string, err error) error {
|
||||
return fmt.Errorf("ssh auth: permission denied for %s", hostname)
|
||||
} else if strings.Contains(out, "Network is unreachable") {
|
||||
return fmt.Errorf("unable to connect to %s, please check your SSH config", hostname)
|
||||
} else if strings.Contains(out, "Is the docker daemon running") {
|
||||
return fmt.Errorf("docker: is the daemon running / your user has docker permissions?")
|
||||
}
|
||||
|
||||
return err
|
||||
|
353
pkg/ui/deploy.go
Normal file
353
pkg/ui/deploy.go
Normal file
@ -0,0 +1,353 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
"coopcloud.tech/abra/pkg/logs"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/docker/cli/cli/command/service/progress"
|
||||
containerTypes "github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
)
|
||||
|
||||
var IsRunning bool
|
||||
|
||||
type statusMsg struct {
|
||||
stream stream
|
||||
jsonMsg jsonmessage.JSONMessage
|
||||
}
|
||||
|
||||
type progressCompleteMsg struct {
|
||||
stream stream
|
||||
failed bool
|
||||
}
|
||||
|
||||
type healthcheckMsg struct {
|
||||
stream stream
|
||||
health string
|
||||
}
|
||||
|
||||
type ServiceMeta struct {
|
||||
Name string
|
||||
ID string
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
appName string
|
||||
cl *dockerClient.Client
|
||||
count int
|
||||
ctx context.Context
|
||||
timeout time.Duration
|
||||
width int
|
||||
filters filters.Args
|
||||
|
||||
Streams *[]stream
|
||||
Logs *[]string
|
||||
Failed bool
|
||||
TimedOut bool
|
||||
Quit bool
|
||||
}
|
||||
|
||||
func (m Model) complete() bool {
|
||||
if m.count == len(*m.Streams) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type stream struct {
|
||||
Name string
|
||||
Err error
|
||||
|
||||
decoder *json.Decoder
|
||||
id string
|
||||
reader *io.PipeReader
|
||||
writer *io.PipeWriter
|
||||
status string
|
||||
retries int
|
||||
health string
|
||||
rollback bool
|
||||
}
|
||||
|
||||
func (s stream) String() string {
|
||||
out := fmt.Sprintf("{decoder: %v, ", s.decoder)
|
||||
out += fmt.Sprintf("err: %v, ", s.Err)
|
||||
out += fmt.Sprintf("id: %s, ", s.id)
|
||||
out += fmt.Sprintf("name: %s, ", s.Name)
|
||||
out += fmt.Sprintf("reader: %v, ", s.reader)
|
||||
out += fmt.Sprintf("writer: %v, ", s.writer)
|
||||
out += fmt.Sprintf("status: %s, ", s.status)
|
||||
return out
|
||||
}
|
||||
|
||||
func (s stream) progress(m Model) tea.Msg {
|
||||
if err := progress.ServiceProgress(m.ctx, m.cl, s.id, s.writer); err != nil {
|
||||
return progressCompleteMsg{
|
||||
stream: s,
|
||||
failed: true,
|
||||
}
|
||||
}
|
||||
|
||||
return progressCompleteMsg{stream: s}
|
||||
}
|
||||
|
||||
func (s stream) process() tea.Msg {
|
||||
var jsonMsg jsonmessage.JSONMessage
|
||||
|
||||
if err := s.decoder.Decode(&jsonMsg); err != nil {
|
||||
if err == io.EOF {
|
||||
// NOTE(d1): end processing messages
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return statusMsg{
|
||||
stream: s,
|
||||
jsonMsg: jsonMsg,
|
||||
}
|
||||
}
|
||||
|
||||
func (s stream) healthcheck(m Model) tea.Msg {
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("name", fmt.Sprintf("^%s", s.Name))
|
||||
|
||||
containers, err := m.cl.ContainerList(m.ctx, containerTypes.ListOptions{Filters: filters})
|
||||
if err != nil {
|
||||
s.Err = err
|
||||
return healthcheckMsg{stream: s}
|
||||
}
|
||||
|
||||
if len(containers) == 0 {
|
||||
return healthcheckMsg{stream: s}
|
||||
}
|
||||
|
||||
container := containers[0]
|
||||
containerState, err := m.cl.ContainerInspect(m.ctx, container.ID)
|
||||
if err != nil {
|
||||
s.Err = err
|
||||
return healthcheckMsg{stream: s}
|
||||
}
|
||||
|
||||
var health string
|
||||
if containerState.State.Health != nil {
|
||||
health = containerState.State.Health.Status
|
||||
}
|
||||
|
||||
return healthcheckMsg{stream: s, health: health}
|
||||
}
|
||||
|
||||
func DeployInitialModel(
|
||||
ctx context.Context,
|
||||
cl *dockerClient.Client,
|
||||
services []ServiceMeta,
|
||||
appName string,
|
||||
timeout time.Duration,
|
||||
filters filters.Args,
|
||||
) Model {
|
||||
var streams []stream
|
||||
for _, service := range services {
|
||||
r, w := io.Pipe()
|
||||
d := json.NewDecoder(r)
|
||||
streams = append(streams, stream{
|
||||
Name: service.Name,
|
||||
id: service.ID,
|
||||
reader: r,
|
||||
writer: w,
|
||||
decoder: d,
|
||||
retries: -1, // NOTE(d1): skip first attempt
|
||||
health: "?",
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(streams, func(i, j int) bool {
|
||||
return streams[i].Name < streams[j].Name
|
||||
})
|
||||
|
||||
return Model{
|
||||
ctx: ctx,
|
||||
cl: cl,
|
||||
appName: appName,
|
||||
timeout: timeout,
|
||||
filters: filters,
|
||||
Streams: &streams,
|
||||
Logs: &[]string{},
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
var cmds []tea.Cmd
|
||||
|
||||
for _, stream := range *m.Streams {
|
||||
cmds = append(
|
||||
cmds,
|
||||
[]tea.Cmd{
|
||||
func() tea.Msg { return stream.progress(m) },
|
||||
func() tea.Msg { return stream.process() },
|
||||
func() tea.Msg { return stream.healthcheck(m) },
|
||||
}...,
|
||||
)
|
||||
}
|
||||
|
||||
cmds = append(cmds, func() tea.Msg { return deployTimeout(m) })
|
||||
cmds = append(cmds, func() tea.Msg { return m.gatherLogs() })
|
||||
|
||||
return tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m Model) gatherLogs() tea.Msg {
|
||||
var services []string
|
||||
for _, s := range *m.Streams {
|
||||
services = append(services, s.Name)
|
||||
}
|
||||
|
||||
opts := logs.TailOpts{
|
||||
AppName: m.appName,
|
||||
Services: services,
|
||||
StdErr: true,
|
||||
Buffer: m.Logs,
|
||||
ToBuffer: true,
|
||||
Filters: m.filters,
|
||||
}
|
||||
|
||||
// NOTE(d1): not interested in log polling errors. if we don't see logs it
|
||||
// will hopefully be self-evident based on what happened in the deployment
|
||||
logs.TailLogs(m.cl, opts)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type timeoutMsg struct{}
|
||||
|
||||
func deployTimeout(m Model) tea.Msg {
|
||||
<-time.After(m.timeout)
|
||||
return timeoutMsg{}
|
||||
}
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmds []tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "q":
|
||||
m.Quit = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
|
||||
case progressCompleteMsg:
|
||||
if msg.failed {
|
||||
m.Failed = true
|
||||
}
|
||||
|
||||
m.count += 1
|
||||
|
||||
if m.complete() {
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
||||
case timeoutMsg:
|
||||
m.TimedOut = true
|
||||
return m, tea.Quit
|
||||
|
||||
case healthcheckMsg:
|
||||
for idx, s := range *m.Streams {
|
||||
if s.id == msg.stream.id {
|
||||
h := "?"
|
||||
if s.health != "" {
|
||||
h = s.health
|
||||
}
|
||||
if msg.health != "" {
|
||||
h = msg.health
|
||||
}
|
||||
(*m.Streams)[idx].health = h
|
||||
}
|
||||
}
|
||||
|
||||
cmds = append(
|
||||
cmds,
|
||||
func() tea.Msg { return msg.stream.healthcheck(m) },
|
||||
)
|
||||
|
||||
case statusMsg:
|
||||
for idx, s := range *m.Streams {
|
||||
if s.id == msg.stream.id {
|
||||
|
||||
if msg.jsonMsg.ID == "rollback" {
|
||||
m.Failed = true
|
||||
(*m.Streams)[idx].rollback = true
|
||||
}
|
||||
|
||||
if msg.jsonMsg.ID != "overall progress" {
|
||||
newStatus := strings.ToLower(msg.jsonMsg.Status)
|
||||
currentStatus := (*m.Streams)[idx].status
|
||||
|
||||
if !strings.Contains(currentStatus, "starting") &&
|
||||
strings.Contains(newStatus, "starting") {
|
||||
(*m.Streams)[idx].retries += 1
|
||||
}
|
||||
|
||||
if (*m.Streams)[idx].rollback {
|
||||
if msg.jsonMsg.ID == "rollback" {
|
||||
(*m.Streams)[idx].status = newStatus
|
||||
}
|
||||
} else {
|
||||
(*m.Streams)[idx].status = newStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cmds = append(
|
||||
cmds,
|
||||
func() tea.Msg { return msg.stream.process() },
|
||||
)
|
||||
}
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m Model) View() string {
|
||||
body := strings.Builder{}
|
||||
|
||||
for _, stream := range *m.Streams {
|
||||
split := strings.Split(stream.Name, "_")
|
||||
short := split[len(split)-1]
|
||||
|
||||
status := stream.status
|
||||
if strings.Contains(stream.status, "converged") && !stream.rollback {
|
||||
status = "succeeded"
|
||||
}
|
||||
if strings.Contains(stream.status, "rolled back") {
|
||||
status = "rolled back"
|
||||
}
|
||||
|
||||
retries := 0
|
||||
if stream.retries > 0 {
|
||||
retries = stream.retries
|
||||
}
|
||||
|
||||
output := fmt.Sprintf("%s: %s (retries: %v, healthcheck: %s)",
|
||||
formatter.BoldStyle.Render(short),
|
||||
status,
|
||||
retries,
|
||||
stream.health,
|
||||
)
|
||||
|
||||
body.WriteString(output)
|
||||
body.WriteString("\n")
|
||||
}
|
||||
|
||||
return body.String()
|
||||
}
|
@ -3,8 +3,11 @@ package stack // https://github.com/docker/cli/blob/master/cli/command/stack/rem
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"github.com/docker/docker/api/types"
|
||||
@ -18,23 +21,34 @@ import (
|
||||
|
||||
// RunRemove is the swarm implementation of docker stack remove
|
||||
func RunRemove(ctx context.Context, client *apiclient.Client, opts Remove) error {
|
||||
sigIntCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigIntCh, os.Interrupt)
|
||||
defer signal.Stop(sigIntCh)
|
||||
|
||||
waitCh := make(chan struct{})
|
||||
errCh := make(chan error)
|
||||
|
||||
go func() {
|
||||
var errs []string
|
||||
for _, namespace := range opts.Namespaces {
|
||||
services, err := GetStackServices(ctx, client, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
|
||||
networks, err := getStackNetworks(ctx, client, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
|
||||
var secrets []swarm.Secret
|
||||
if versions.GreaterThanOrEqualTo(client.ClientVersion(), "1.25") {
|
||||
secrets, err = getStackSecrets(ctx, client, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,7 +56,8 @@ func RunRemove(ctx context.Context, client *apiclient.Client, opts Remove) error
|
||||
if versions.GreaterThanOrEqualTo(client.ClientVersion(), "1.30") {
|
||||
configs, err = getStackConfigs(ctx, client, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,14 +76,32 @@ func RunRemove(ctx context.Context, client *apiclient.Client, opts Remove) error
|
||||
continue
|
||||
}
|
||||
|
||||
err = waitOnTasks(ctx, client, namespace)
|
||||
log.Info("polling undeploy status")
|
||||
timeout, err := waitOnTasks(ctx, client, namespace)
|
||||
if timeout {
|
||||
errs = append(errs, err.Error())
|
||||
} else {
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Sprintf("failed to wait on tasks of stack: %s: %s", namespace, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errors.Errorf(strings.Join(errs, "\n"))
|
||||
errCh <- errors.Errorf(strings.Join(errs, "\n"))
|
||||
return
|
||||
}
|
||||
|
||||
close(waitCh)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-waitCh:
|
||||
return nil
|
||||
case <-sigIntCh:
|
||||
return fmt.Errorf("skipping as requested, undeploy still in progress 🟠")
|
||||
case err := <-errCh:
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -88,7 +121,7 @@ func removeServices(
|
||||
var hasError bool
|
||||
sort.Slice(services, sortServiceByName(services))
|
||||
for _, service := range services {
|
||||
log.Infof("removing service %s", service.Spec.Name)
|
||||
log.Debugf("removing service %s", service.Spec.Name)
|
||||
if err := client.ServiceRemove(ctx, service.ID); err != nil {
|
||||
hasError = true
|
||||
log.Fatalf("failed to remove service %s: %s", service.ID, err)
|
||||
@ -104,7 +137,7 @@ func removeNetworks(
|
||||
) bool {
|
||||
var hasError bool
|
||||
for _, network := range networks {
|
||||
log.Infof("removing network %s", network.Name)
|
||||
log.Debugf("removing network %s", network.Name)
|
||||
if err := client.NetworkRemove(ctx, network.ID); err != nil {
|
||||
hasError = true
|
||||
log.Fatalf("failed to remove network %s: %s", network.ID, err)
|
||||
@ -120,7 +153,7 @@ func removeSecrets(
|
||||
) bool {
|
||||
var hasError bool
|
||||
for _, secret := range secrets {
|
||||
log.Infof("removing secret %s", secret.Spec.Name)
|
||||
log.Debugf("removing secret %s", secret.Spec.Name)
|
||||
if err := client.SecretRemove(ctx, secret.ID); err != nil {
|
||||
hasError = true
|
||||
log.Fatalf("Failed to remove secret %s: %s", secret.ID, err)
|
||||
@ -136,7 +169,7 @@ func removeConfigs(
|
||||
) bool {
|
||||
var hasError bool
|
||||
for _, config := range configs {
|
||||
log.Infof("removing config %s", config.Spec.Name)
|
||||
log.Debugf("removing config %s", config.Spec.Name)
|
||||
if err := client.ConfigRemove(ctx, config.ID); err != nil {
|
||||
hasError = true
|
||||
log.Fatalf("failed to remove config %s: %s", config.ID, err)
|
||||
@ -170,12 +203,23 @@ func terminalState(state swarm.TaskState) bool {
|
||||
return numberedStates[state] > numberedStates[swarm.TaskStateRunning]
|
||||
}
|
||||
|
||||
func waitOnTasks(ctx context.Context, client apiclient.APIClient, namespace string) error {
|
||||
func waitOnTasks(ctx context.Context, client apiclient.APIClient, namespace string) (bool, error) {
|
||||
var timedOut bool
|
||||
|
||||
log.Debugf("waiting on undeploy tasks (timeout=%v secs)", WaitTimeout)
|
||||
|
||||
go func() {
|
||||
t := time.Duration(WaitTimeout) * time.Second
|
||||
<-time.After(t)
|
||||
log.Debug("timed out on undeploy")
|
||||
timedOut = true
|
||||
}()
|
||||
|
||||
terminalStatesReached := 0
|
||||
for {
|
||||
tasks, err := getStackTasks(ctx, client, namespace)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get tasks: %w", err)
|
||||
return false, fmt.Errorf("failed to get tasks: %w", err)
|
||||
}
|
||||
|
||||
for _, task := range tasks {
|
||||
@ -188,6 +232,11 @@ func waitOnTasks(ctx context.Context, client apiclient.APIClient, namespace stri
|
||||
if terminalStatesReached == len(tasks) {
|
||||
break
|
||||
}
|
||||
|
||||
if timedOut {
|
||||
return true, fmt.Errorf("deployment timed out 🟠")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
@ -3,20 +3,20 @@ package stack // https://github.com/docker/cli/blob/master/cli/command/stack/swa
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
stdlibErr "errors"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"coopcloud.tech/abra/pkg/ui"
|
||||
"coopcloud.tech/abra/pkg/upstream/convert"
|
||||
"github.com/docker/cli/cli/command/service/progress"
|
||||
"github.com/docker/cli/cli/command/stack/formatter"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
@ -177,7 +177,7 @@ func IsDeployed(ctx context.Context, cl *dockerClient.Client, stackName string)
|
||||
func pruneServices(ctx context.Context, cl *dockerClient.Client, namespace convert.Namespace, services map[string]struct{}) {
|
||||
oldServices, err := GetStackServices(ctx, cl, namespace.Name())
|
||||
if err != nil {
|
||||
log.Infof("failed to list services: %s", err)
|
||||
log.Warnf("failed to list services: %s", err)
|
||||
}
|
||||
|
||||
pruneServices := []swarm.Service{}
|
||||
@ -191,7 +191,17 @@ func pruneServices(ctx context.Context, cl *dockerClient.Client, namespace conve
|
||||
}
|
||||
|
||||
// RunDeploy is the swarm implementation of docker stack deploy
|
||||
func RunDeploy(cl *dockerClient.Client, opts Deploy, cfg *composetypes.Config, appName string, dontWait bool) error {
|
||||
func RunDeploy(
|
||||
cl *dockerClient.Client,
|
||||
opts Deploy,
|
||||
cfg *composetypes.Config,
|
||||
appName string,
|
||||
serverName string,
|
||||
dontWait bool,
|
||||
filters filters.Args,
|
||||
) error {
|
||||
log.Info("initialising deployment")
|
||||
|
||||
if err := validateResolveImageFlag(&opts); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -201,7 +211,16 @@ func RunDeploy(cl *dockerClient.Client, opts Deploy, cfg *composetypes.Config, a
|
||||
opts.ResolveImage = ResolveImageNever
|
||||
}
|
||||
|
||||
return deployCompose(context.Background(), cl, opts, cfg, appName, dontWait)
|
||||
return deployCompose(
|
||||
context.Background(),
|
||||
cl,
|
||||
opts,
|
||||
cfg,
|
||||
appName,
|
||||
serverName,
|
||||
dontWait,
|
||||
filters,
|
||||
)
|
||||
}
|
||||
|
||||
// validateResolveImageFlag validates the opts.resolveImage command line option
|
||||
@ -214,7 +233,16 @@ func validateResolveImageFlag(opts *Deploy) error {
|
||||
}
|
||||
}
|
||||
|
||||
func deployCompose(ctx context.Context, cl *dockerClient.Client, opts Deploy, config *composetypes.Config, appName string, dontWait bool) error {
|
||||
func deployCompose(
|
||||
ctx context.Context,
|
||||
cl *dockerClient.Client,
|
||||
opts Deploy,
|
||||
config *composetypes.Config,
|
||||
appName string,
|
||||
serverName string,
|
||||
dontWait bool,
|
||||
filters filters.Args,
|
||||
) error {
|
||||
namespace := convert.NewNamespace(opts.Namespace)
|
||||
|
||||
if opts.Prune {
|
||||
@ -255,7 +283,14 @@ func deployCompose(ctx context.Context, cl *dockerClient.Client, opts Deploy, co
|
||||
return err
|
||||
}
|
||||
|
||||
serviceIDs, err := deployServices(ctx, cl, services, namespace, opts.SendRegistryAuth, opts.ResolveImage)
|
||||
serviceIDs, err := deployServices(
|
||||
ctx,
|
||||
cl,
|
||||
services,
|
||||
namespace,
|
||||
opts.SendRegistryAuth,
|
||||
opts.ResolveImage,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -265,13 +300,16 @@ func deployCompose(ctx context.Context, cl *dockerClient.Client, opts Deploy, co
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Infof("waiting for %s to deploy... please hold 🤚", appName)
|
||||
|
||||
if err := waitOnServices(ctx, cl, serviceIDs, appName); err != nil {
|
||||
return err
|
||||
waitOpts := WaitOpts{
|
||||
Services: serviceIDs,
|
||||
AppName: appName,
|
||||
ServerName: serverName,
|
||||
Filters: filters,
|
||||
}
|
||||
|
||||
log.Infof("successfully deployed %s", appName)
|
||||
if err := WaitOnServices(ctx, cl, waitOpts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -343,7 +381,7 @@ func createConfigs(ctx context.Context, cl *dockerClient.Client, configs []swarm
|
||||
}
|
||||
case dockerClient.IsErrNotFound(err):
|
||||
// config does not exist, then we create a new one.
|
||||
log.Infof("creating config %s", configSpec.Name)
|
||||
log.Debugf("creating config %s", configSpec.Name)
|
||||
if _, err := cl.ConfigCreate(ctx, configSpec); err != nil {
|
||||
return errors.Wrapf(err, "failed to create config %s", configSpec.Name)
|
||||
}
|
||||
@ -374,7 +412,7 @@ func createNetworks(ctx context.Context, cl *dockerClient.Client, namespace conv
|
||||
createOpts.Driver = defaultNetworkDriver
|
||||
}
|
||||
|
||||
log.Infof("creating network %s", name)
|
||||
log.Debugf("creating network %s", name)
|
||||
if _, err := cl.NetworkCreate(ctx, name, createOpts); err != nil {
|
||||
return errors.Wrapf(err, "failed to create network %s", name)
|
||||
}
|
||||
@ -388,10 +426,12 @@ func deployServices(
|
||||
services map[string]swarm.ServiceSpec,
|
||||
namespace convert.Namespace,
|
||||
sendAuth bool,
|
||||
resolveImage string) ([]string, error) {
|
||||
resolveImage string) ([]ui.ServiceMeta, error) {
|
||||
var servicesMeta []ui.ServiceMeta
|
||||
|
||||
existingServices, err := GetStackServices(ctx, cl, namespace.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return servicesMeta, err
|
||||
}
|
||||
|
||||
existingServiceMap := make(map[string]swarm.Service)
|
||||
@ -399,8 +439,6 @@ func deployServices(
|
||||
existingServiceMap[service.Spec.Name] = service
|
||||
}
|
||||
|
||||
var serviceIDs []string
|
||||
|
||||
for internalName, serviceSpec := range services {
|
||||
var (
|
||||
name = namespace.Scope(internalName)
|
||||
@ -409,7 +447,7 @@ func deployServices(
|
||||
)
|
||||
|
||||
if service, exists := existingServiceMap[name]; exists {
|
||||
log.Infof("updating %s", name)
|
||||
log.Debugf("updating %s", name)
|
||||
|
||||
updateOpts := types.ServiceUpdateOptions{EncodedRegistryAuth: encodedAuth}
|
||||
|
||||
@ -451,9 +489,12 @@ func deployServices(
|
||||
log.Warn(warning)
|
||||
}
|
||||
|
||||
serviceIDs = append(serviceIDs, service.ID)
|
||||
servicesMeta = append(servicesMeta, ui.ServiceMeta{
|
||||
Name: name,
|
||||
ID: service.ID,
|
||||
})
|
||||
} else {
|
||||
log.Infof("creating %s", name)
|
||||
log.Debugf("creating %s", name)
|
||||
|
||||
createOpts := types.ServiceCreateOptions{EncodedRegistryAuth: encodedAuth}
|
||||
|
||||
@ -467,11 +508,14 @@ func deployServices(
|
||||
return nil, errors.Wrapf(err, "failed to create %s", name)
|
||||
}
|
||||
|
||||
serviceIDs = append(serviceIDs, serviceCreateResponse.ID)
|
||||
servicesMeta = append(servicesMeta, ui.ServiceMeta{
|
||||
Name: name,
|
||||
ID: serviceCreateResponse.ID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return serviceIDs, nil
|
||||
return servicesMeta, nil
|
||||
}
|
||||
|
||||
func getStackNetworks(ctx context.Context, dockerclient client.APIClient, namespace string) ([]networktypes.Inspect, error) {
|
||||
@ -486,67 +530,89 @@ func getStackConfigs(ctx context.Context, dockerclient client.APIClient, namespa
|
||||
return dockerclient.ConfigList(ctx, types.ConfigListOptions{Filters: getStackFilter(namespace)})
|
||||
}
|
||||
|
||||
func waitOnServices(ctx context.Context, cl *dockerClient.Client, serviceIDs []string, appName string) error {
|
||||
func timestamp() string {
|
||||
ts := time.Now().UTC().Format(time.RFC3339)
|
||||
return strings.Replace(ts, ":", "", -1) // get rid of offensive colons
|
||||
}
|
||||
|
||||
type WaitOpts struct {
|
||||
AppName string
|
||||
Filters filters.Args
|
||||
NoLog bool
|
||||
Quiet bool
|
||||
ServerName string
|
||||
Services []ui.ServiceMeta
|
||||
}
|
||||
|
||||
func WaitOnServices(ctx context.Context, cl *dockerClient.Client, opts WaitOpts) error {
|
||||
timeout := time.Duration(WaitTimeout) * time.Second
|
||||
model := ui.DeployInitialModel(ctx, cl, opts.Services, opts.AppName, timeout, opts.Filters)
|
||||
tui := tea.NewProgram(model)
|
||||
|
||||
if !opts.Quiet {
|
||||
log.Info("polling deployment status")
|
||||
}
|
||||
|
||||
m, err := log.Without(
|
||||
func() (tea.Model, error) {
|
||||
return tui.Run()
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("waitOnServices: error running TUI: %s", err)
|
||||
}
|
||||
|
||||
deployModel := m.(ui.Model)
|
||||
if deployModel.TimedOut || deployModel.Failed || deployModel.Quit {
|
||||
var errs []error
|
||||
|
||||
for _, serviceID := range serviceIDs {
|
||||
if err := WaitOnService(ctx, cl, serviceID, appName); err != nil {
|
||||
errs = append(errs, fmt.Errorf("%s: %w", serviceID, err))
|
||||
if deployModel.Failed {
|
||||
errs = append(errs, fmt.Errorf("deploy failed 🛑"))
|
||||
} else if deployModel.TimedOut {
|
||||
errs = append(errs, fmt.Errorf("deploy timed out 🟠"))
|
||||
} else {
|
||||
errs = append(errs, fmt.Errorf("deploy in progress 🟠"))
|
||||
}
|
||||
|
||||
for _, s := range *deployModel.Streams {
|
||||
if s.Err != nil {
|
||||
errs = append(errs, fmt.Errorf("%s: %s", s.Name, s.Err))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
if len(*deployModel.Logs) > 0 && !opts.NoLog {
|
||||
logsPath := filepath.Join(
|
||||
config.LOGS_DIR,
|
||||
opts.ServerName,
|
||||
fmt.Sprintf("%s_%s", opts.AppName, timestamp()),
|
||||
)
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(config.LOGS_DIR, opts.ServerName), 0764); err != nil {
|
||||
return fmt.Errorf("waitOnServices: error creating log dir: %s", err)
|
||||
}
|
||||
|
||||
file, err := os.Create(logsPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("waitOnServices: error opening file: %s", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
s := strings.Join(*deployModel.Logs, "\n")
|
||||
if _, err := file.WriteString(s); err != nil {
|
||||
return fmt.Errorf("waitOnServices: writeFile: %s", err)
|
||||
}
|
||||
|
||||
errs = append(errs, fmt.Errorf("logs: %s", logsPath))
|
||||
}
|
||||
|
||||
return stdlibErr.Join(errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// https://github.com/docker/cli/blob/master/cli/command/service/helpers.go
|
||||
// https://github.com/docker/cli/blob/master/cli/command/service/progress/progress.go
|
||||
func WaitOnService(ctx context.Context, cl *dockerClient.Client, serviceID, appName string) error {
|
||||
errChan := make(chan error, 1)
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
|
||||
sigintChannel := make(chan os.Signal, 1)
|
||||
signal.Notify(sigintChannel, os.Interrupt)
|
||||
defer signal.Stop(sigintChannel)
|
||||
|
||||
go func() {
|
||||
errChan <- progress.ServiceProgress(ctx, cl, serviceID, pipeWriter)
|
||||
}()
|
||||
|
||||
go io.Copy(ioutil.Discard, pipeReader)
|
||||
|
||||
timeout := time.Duration(WaitTimeout) * time.Second
|
||||
|
||||
select {
|
||||
case err := <-errChan:
|
||||
return err
|
||||
case <-sigintChannel:
|
||||
return fmt.Errorf(`
|
||||
Not waiting for %s to deploy. The deployment is ongoing...
|
||||
|
||||
If you want to stop the deployment, try:
|
||||
|
||||
abra app undeploy %s`, appName, appName)
|
||||
case <-time.After(timeout):
|
||||
return fmt.Errorf(`
|
||||
%s has not converged (%s second timeout reached).
|
||||
|
||||
This does not necessarily mean your deployment has failed, it may just be that
|
||||
the app is taking longer to deploy based on your server resources or network
|
||||
latency.
|
||||
|
||||
You can track latest deployment status with:
|
||||
|
||||
abra app ps %s
|
||||
|
||||
And inspect the logs with:
|
||||
|
||||
abra app logs %s
|
||||
`, appName, timeout, appName, appName)
|
||||
if !opts.Quiet {
|
||||
log.Info("deploy succeeded 🟢")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Copypasta from https://github.com/docker/cli/blob/master/cli/command/stack/swarm/list.go
|
||||
|
@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
ABRA_VERSION="0.9.0-beta"
|
||||
ABRA_VERSION="0.10.1-beta"
|
||||
ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/toolshed/abra/releases/tags/$ABRA_VERSION"
|
||||
RC_VERSION="0.10.0-rc1-beta"
|
||||
RC_VERSION="0.10.1-beta"
|
||||
RC_VERSION_URL="https://git.coopcloud.tech/api/v1/repos/toolshed/abra/releases/tags/$RC_VERSION"
|
||||
|
||||
for arg in "$@"; do
|
||||
|
@ -3,5 +3,5 @@ STACK := abra_installer_script
|
||||
default: deploy
|
||||
|
||||
deploy:
|
||||
@DOCKER_CONTEXT=swarm.autonomic.zone docker stack rm $(STACK) && \
|
||||
DOCKER_CONTEXT=swarm.autonomic.zone docker stack deploy -c compose.yml $(STACK)
|
||||
@DOCKER_CONTEXT=swarm-0.coopcloud.tech docker stack rm $(STACK) && \
|
||||
DOCKER_CONTEXT=swarm-0.coopcloud.tech docker stack deploy -c compose.yml $(STACK)
|
||||
|
@ -108,8 +108,6 @@ teardown(){
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "deploy latest commit if no published versions and no --chaos" {
|
||||
latestCommit="$(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-parse --short HEAD)"
|
||||
|
||||
_remove_tags
|
||||
_wipe_env_version
|
||||
|
||||
@ -117,7 +115,7 @@ teardown(){
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" \
|
||||
--no-input --no-converge-checks --offline
|
||||
assert_success
|
||||
assert_output --partial "$latestCommit"
|
||||
assert_output --partial "${_get_head_hash:0:8}"
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@ -403,8 +401,6 @@ teardown(){
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "ignore env version on new deploy" {
|
||||
tagHash=$(_get_tag_hash "0.1.0+1.20.0")
|
||||
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" \
|
||||
--no-input --no-converge-checks
|
||||
assert_success
|
||||
|
@ -8,6 +8,7 @@ setup_file(){
|
||||
}
|
||||
|
||||
teardown_file(){
|
||||
_undeploy_app
|
||||
_rm_app
|
||||
_rm_server
|
||||
}
|
||||
@ -18,6 +19,7 @@ setup(){
|
||||
}
|
||||
|
||||
teardown(){
|
||||
_reset_recipe
|
||||
_undeploy_app
|
||||
}
|
||||
|
||||
@ -87,6 +89,10 @@ teardown(){
|
||||
assert_success
|
||||
refute_output --partial "$TEST_RECIPE"
|
||||
assert_output --partial "foo-recipe"
|
||||
|
||||
run rm -rf "$ABRA_DIR/servers/foo.com"
|
||||
assert_success
|
||||
assert_not_exists "$ABRA_DIR/servers/foo.com"
|
||||
}
|
||||
|
||||
@test "output is machine readable" {
|
||||
@ -98,3 +104,36 @@ teardown(){
|
||||
|
||||
assert_output --partial "$expectedOutput"
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "list with status fetches recipe" {
|
||||
_deploy_app
|
||||
|
||||
run $ABRA app ls --status
|
||||
assert_success
|
||||
|
||||
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE"
|
||||
assert_success
|
||||
|
||||
run $ABRA app ls --status
|
||||
assert_success
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "list with chaos version" {
|
||||
run bash -c "echo foo >> $ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" \
|
||||
--no-input --no-converge-checks --chaos
|
||||
assert_success
|
||||
|
||||
run $ABRA app ls --status
|
||||
assert_success
|
||||
assert_output --partial "+U"
|
||||
|
||||
run rm -rf "$ABRA_DIR/servers/foo.com"
|
||||
assert_success
|
||||
assert_not_exists "$ABRA_DIR/servers/foo.com"
|
||||
}
|
||||
|
@ -214,8 +214,7 @@ teardown(){
|
||||
--chaos
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_output --partial "version: ${currentHash:0:8}"
|
||||
assert_output --partial "chaos: ${currentHash:0:8}"
|
||||
assert_output --partial "version: ${currentHash:0:8}+U"
|
||||
|
||||
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_equal "$(_git_status)" "?? foo"
|
||||
@ -242,8 +241,7 @@ teardown(){
|
||||
--chaos
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_output --partial "version: ${currentHash:0:8}"
|
||||
assert_output --partial "chaos: ${currentHash:0:8}"
|
||||
assert_output --partial "version: ${currentHash:0:8}+U"
|
||||
|
||||
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_equal "$(_git_status)" "?? foo"
|
||||
@ -252,3 +250,10 @@ teardown(){
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "automatically select single server" {
|
||||
# NOTE(d1): no --no-input required, single server available
|
||||
run $ABRA app new "$TEST_RECIPE" --domain "$TEST_APP_DOMAIN"
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
}
|
||||
|
@ -204,6 +204,18 @@ teardown(){
|
||||
assert_output --partial 'release notes baz' # 0.2.0+1.21.0
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "show release note added after release" {
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.2.0+1.21.0" --no-input --no-converge-checks
|
||||
assert_success
|
||||
assert_output --partial '0.2.0+1.21.0'
|
||||
|
||||
run $ABRA app upgrade "$TEST_APP_DOMAIN" "0.3.0+1.21.0" --no-input --no-converge-checks
|
||||
assert_success
|
||||
assert_output --partial '0.3.0+1.21.0'
|
||||
assert_output --partial 'A release note added after the release'
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "upgrade commit deployment not possible" {
|
||||
tagHash=$(_get_tag_hash "0.1.0+1.20.0")
|
||||
|
@ -25,13 +25,3 @@ teardown(){
|
||||
run "$HOME/.local/bin/abra" -v
|
||||
assert_output --partial 'beta'
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "install release candidate from script" {
|
||||
run bash -c 'curl https://install.abra.coopcloud.tech | bash -s -- --rc'
|
||||
assert_success
|
||||
|
||||
assert_exists "$HOME/.local/bin/abra"
|
||||
run "$HOME/.local/bin/abra" -v
|
||||
assert_output --partial '-rc'
|
||||
}
|
||||
|
@ -5,6 +5,16 @@ setup() {
|
||||
_common_setup
|
||||
}
|
||||
|
||||
teardown(){
|
||||
run rm -rf "$ABRA_DIR/recipes/matrix-synapse"
|
||||
assert_success
|
||||
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE"
|
||||
|
||||
run rm -rf "$ABRA_DIR/recipes/git_coopcloud_tech_coop-cloud_matrix-synapse"
|
||||
assert_success
|
||||
assert_not_exists "$ABRA_DIR/recipes/git_coopcloud_tech_coop-cloud_matrix-synapse"
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "recipe fetch all" {
|
||||
run rm -rf "$ABRA_DIR/recipes/matrix-synapse"
|
||||
@ -35,3 +45,81 @@ setup() {
|
||||
run $ABRA recipe fetch matrix-synapse --all
|
||||
assert_failure
|
||||
}
|
||||
|
||||
@test "do not refetch without --force" {
|
||||
run $ABRA recipe fetch matrix-synapse
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/recipes/matrix-synapse"
|
||||
|
||||
run $ABRA recipe fetch matrix-synapse
|
||||
assert_output --partial "already fetched"
|
||||
}
|
||||
|
||||
@test "refetch with --force" {
|
||||
run $ABRA recipe fetch matrix-synapse
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/recipes/matrix-synapse"
|
||||
|
||||
run $ABRA recipe fetch matrix-synapse --force
|
||||
assert_success
|
||||
refute_output --partial "already fetched"
|
||||
}
|
||||
|
||||
@test "refetch with --force does not erase unstaged changes" {
|
||||
run $ABRA recipe fetch matrix-synapse
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/recipes/matrix-synapse"
|
||||
|
||||
run bash -c "echo foo >> $ABRA_DIR/recipes/matrix-synapse/foo"
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/recipes/matrix-synapse/foo"
|
||||
|
||||
run $ABRA recipe fetch matrix-synapse --force
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/recipes/matrix-synapse"
|
||||
assert_exists "$ABRA_DIR/recipes/matrix-synapse/foo"
|
||||
}
|
||||
|
||||
@test "fetch with --ssh" {
|
||||
run $ABRA recipe fetch matrix-synapse --ssh
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/recipes/matrix-synapse"
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/matrix-synapse" remote -v
|
||||
assert_success
|
||||
assert_output --partial "ssh://"
|
||||
}
|
||||
|
||||
@test "re-fetch with --ssh/--force" {
|
||||
run $ABRA recipe fetch matrix-synapse
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/recipes/matrix-synapse"
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/matrix-synapse" remote -v
|
||||
assert_success
|
||||
assert_output --partial "https://"
|
||||
|
||||
run $ABRA recipe fetch matrix-synapse --ssh --force
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/recipes/matrix-synapse"
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/matrix-synapse" remote -v
|
||||
assert_success
|
||||
assert_output --partial "ssh://"
|
||||
}
|
||||
|
||||
@test "fetch remote recipe" {
|
||||
run $ABRA recipe fetch git.coopcloud.tech/coop-cloud/matrix-synapse
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/recipes/git_coopcloud_tech_coop-cloud_matrix-synapse"
|
||||
}
|
||||
|
||||
@test "remote recipe do not refetch without --force" {
|
||||
run $ABRA recipe fetch git.coopcloud.tech/coop-cloud/matrix-synapse
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/recipes/git_coopcloud_tech_coop-cloud_matrix-synapse"
|
||||
|
||||
run $ABRA recipe fetch git.coopcloud.tech/coop-cloud/matrix-synapse
|
||||
assert_success
|
||||
assert_output --partial "already fetched"
|
||||
}
|
||||
|
@ -68,3 +68,27 @@ teardown(){
|
||||
assert_output --partial 'fooUser'
|
||||
assert_output --partial 'foo@example.com'
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "recipe new, app new, no releases, latest commit" {
|
||||
recipeName="foobar"
|
||||
|
||||
run $ABRA recipe new "$recipeName"
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/recipes/$recipeName"
|
||||
|
||||
currentHash=$(git -C "$ABRA_DIR/recipes/$recipeName" show -s --format="%H")
|
||||
domain="$recipeName.$TEST_APP_SERVER"
|
||||
|
||||
run $ABRA app new "$recipeName" \
|
||||
--no-input \
|
||||
--server "$TEST_SERVER" \
|
||||
--domain "$domain"
|
||||
assert_success
|
||||
assert_output --partial "version: ${currentHash:0:8}"
|
||||
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$domain.env"
|
||||
|
||||
run grep -q "TYPE=$recipeName:${currentHash:0:8}" \
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$domain.env"
|
||||
assert_success
|
||||
}
|
||||
|
@ -26,14 +26,3 @@ teardown(){
|
||||
run "$HOME/.local/bin/abra" -v
|
||||
assert_output --partial 'beta'
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "abra upgrade release candidate" {
|
||||
run $ABRA upgrade --rc
|
||||
assert_success
|
||||
assert_output --partial 'Public interest infrastructure'
|
||||
|
||||
assert_exists "$HOME/.local/bin/abra"
|
||||
run "$HOME/.local/bin/abra" -v
|
||||
assert_output --partial '-rc'
|
||||
}
|
||||
|
1
vendor/github.com/charmbracelet/bubbletea/.gitattributes
generated
vendored
Normal file
1
vendor/github.com/charmbracelet/bubbletea/.gitattributes
generated
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.golden -text
|
23
vendor/github.com/charmbracelet/bubbletea/.gitignore
generated
vendored
Normal file
23
vendor/github.com/charmbracelet/bubbletea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
.DS_Store
|
||||
.envrc
|
||||
|
||||
examples/fullscreen/fullscreen
|
||||
examples/help/help
|
||||
examples/http/http
|
||||
examples/list-default/list-default
|
||||
examples/list-fancy/list-fancy
|
||||
examples/list-simple/list-simple
|
||||
examples/mouse/mouse
|
||||
examples/pager/pager
|
||||
examples/progress-download/color_vortex.blend
|
||||
examples/progress-download/progress-download
|
||||
examples/simple/simple
|
||||
examples/spinner/spinner
|
||||
examples/textinput/textinput
|
||||
examples/textinputs/textinputs
|
||||
examples/views/views
|
||||
tutorials/basics/basics
|
||||
tutorials/commands/commands
|
||||
.idea
|
||||
coverage.txt
|
||||
dist/
|
40
vendor/github.com/charmbracelet/bubbletea/.golangci-soft.yml
generated
vendored
Normal file
40
vendor/github.com/charmbracelet/bubbletea/.golangci-soft.yml
generated
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
run:
|
||||
tests: false
|
||||
issues-exit-code: 0
|
||||
|
||||
issues:
|
||||
include:
|
||||
- EXC0001
|
||||
- EXC0005
|
||||
- EXC0011
|
||||
- EXC0012
|
||||
- EXC0013
|
||||
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- exhaustive
|
||||
- goconst
|
||||
- godot
|
||||
- godox
|
||||
- mnd
|
||||
- gomoddirectives
|
||||
- goprintffuncname
|
||||
- misspell
|
||||
- nakedret
|
||||
- nestif
|
||||
- noctx
|
||||
- nolintlint
|
||||
- prealloc
|
||||
- wrapcheck
|
||||
|
||||
# disable default linters, they are already enabled in .golangci.yml
|
||||
disable:
|
||||
- errcheck
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- staticcheck
|
||||
- unused
|
28
vendor/github.com/charmbracelet/bubbletea/.golangci.yml
generated
vendored
Normal file
28
vendor/github.com/charmbracelet/bubbletea/.golangci.yml
generated
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
run:
|
||||
tests: false
|
||||
|
||||
issues:
|
||||
include:
|
||||
- EXC0001
|
||||
- EXC0005
|
||||
- EXC0011
|
||||
- EXC0012
|
||||
- EXC0013
|
||||
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- bodyclose
|
||||
- gofumpt
|
||||
- goimports
|
||||
- gosec
|
||||
- nilerr
|
||||
- revive
|
||||
- rowserrcheck
|
||||
- sqlclosecheck
|
||||
- tparallel
|
||||
- unconvert
|
||||
- unparam
|
||||
- whitespace
|
5
vendor/github.com/charmbracelet/bubbletea/.goreleaser.yml
generated
vendored
Normal file
5
vendor/github.com/charmbracelet/bubbletea/.goreleaser.yml
generated
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
# yaml-language-server: $schema=https://goreleaser.com/static/schema-pro.json
|
||||
version: 2
|
||||
includes:
|
||||
- from_url:
|
||||
url: charmbracelet/meta/main/goreleaser-lib.yaml
|
21
vendor/github.com/charmbracelet/bubbletea/LICENSE
generated
vendored
Normal file
21
vendor/github.com/charmbracelet/bubbletea/LICENSE
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2023 Charmbracelet, Inc
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
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.
|
396
vendor/github.com/charmbracelet/bubbletea/README.md
generated
vendored
Normal file
396
vendor/github.com/charmbracelet/bubbletea/README.md
generated
vendored
Normal file
@ -0,0 +1,396 @@
|
||||
# Bubble Tea
|
||||
|
||||
<p>
|
||||
<a href="https://stuff.charm.sh/bubbletea/bubbletea-4k.png"><img src="https://github.com/charmbracelet/bubbletea/assets/25087/108d4fdb-d554-4910-abed-2a5f5586a60e" width="313" alt="Bubble Tea Title Treatment"></a><br>
|
||||
<a href="https://github.com/charmbracelet/bubbletea/releases"><img src="https://img.shields.io/github/release/charmbracelet/bubbletea.svg" alt="Latest Release"></a>
|
||||
<a href="https://pkg.go.dev/github.com/charmbracelet/bubbletea?tab=doc"><img src="https://godoc.org/github.com/charmbracelet/bubbletea?status.svg" alt="GoDoc"></a>
|
||||
<a href="https://github.com/charmbracelet/bubbletea/actions"><img src="https://github.com/charmbracelet/bubbletea/actions/workflows/build.yml/badge.svg" alt="Build Status"></a>
|
||||
<a href="https://www.phorm.ai/query?projectId=a0e324b6-b706-4546-b951-6671ea60c13f"><img src="https://stuff.charm.sh/misc/phorm-badge.svg" alt="phorm.ai"></a>
|
||||
</p>
|
||||
|
||||
The fun, functional and stateful way to build terminal apps. A Go framework
|
||||
based on [The Elm Architecture][elm]. Bubble Tea is well-suited for simple and
|
||||
complex terminal applications, either inline, full-window, or a mix of both.
|
||||
|
||||
<p>
|
||||
<img src="https://stuff.charm.sh/bubbletea/bubbletea-example.gif" width="100%" alt="Bubble Tea Example">
|
||||
</p>
|
||||
|
||||
Bubble Tea is in use in production and includes a number of features and
|
||||
performance optimizations we’ve added along the way. Among those is
|
||||
a framerate-based renderer, mouse support, focus reporting and more.
|
||||
|
||||
To get started, see the tutorial below, the [examples][examples], the
|
||||
[docs][docs], the [video tutorials][youtube] and some common [resources](#libraries-we-use-with-bubble-tea).
|
||||
|
||||
[youtube]: https://charm.sh/yt
|
||||
|
||||
## By the way
|
||||
|
||||
Be sure to check out [Bubbles][bubbles], a library of common UI components for Bubble Tea.
|
||||
|
||||
<p>
|
||||
<a href="https://github.com/charmbracelet/bubbles"><img src="https://stuff.charm.sh/bubbles/bubbles-badge.png" width="174" alt="Bubbles Badge"></a>
|
||||
<a href="https://github.com/charmbracelet/bubbles"><img src="https://stuff.charm.sh/bubbles-examples/textinput.gif" width="400" alt="Text Input Example from Bubbles"></a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## Tutorial
|
||||
|
||||
Bubble Tea is based on the functional design paradigms of [The Elm
|
||||
Architecture][elm], which happens to work nicely with Go. It's a delightful way
|
||||
to build applications.
|
||||
|
||||
This tutorial assumes you have a working knowledge of Go.
|
||||
|
||||
By the way, the non-annotated source code for this program is available
|
||||
[on GitHub][tut-source].
|
||||
|
||||
[elm]: https://guide.elm-lang.org/architecture/
|
||||
[tut-source]: https://github.com/charmbracelet/bubbletea/tree/main/tutorials/basics
|
||||
|
||||
### Enough! Let's get to it.
|
||||
|
||||
For this tutorial, we're making a shopping list.
|
||||
|
||||
To start we'll define our package and import some libraries. Our only external
|
||||
import will be the Bubble Tea library, which we'll call `tea` for short.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
// These imports will be used later on the tutorial. If you save the file
|
||||
// now, Go might complain they are unused, but that's fine.
|
||||
// You may also need to run `go mod tidy` to download bubbletea and its
|
||||
// dependencies.
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
```
|
||||
|
||||
Bubble Tea programs are comprised of a **model** that describes the application
|
||||
state and three simple methods on that model:
|
||||
|
||||
- **Init**, a function that returns an initial command for the application to run.
|
||||
- **Update**, a function that handles incoming events and updates the model accordingly.
|
||||
- **View**, a function that renders the UI based on the data in the model.
|
||||
|
||||
### The Model
|
||||
|
||||
So let's start by defining our model which will store our application's state.
|
||||
It can be any type, but a `struct` usually makes the most sense.
|
||||
|
||||
```go
|
||||
type model struct {
|
||||
choices []string // items on the to-do list
|
||||
cursor int // which to-do list item our cursor is pointing at
|
||||
selected map[int]struct{} // which to-do items are selected
|
||||
}
|
||||
```
|
||||
|
||||
### Initialization
|
||||
|
||||
Next, we’ll define our application’s initial state. In this case, we’re defining
|
||||
a function to return our initial model, however, we could just as easily define
|
||||
the initial model as a variable elsewhere, too.
|
||||
|
||||
```go
|
||||
func initialModel() model {
|
||||
return model{
|
||||
// Our to-do list is a grocery list
|
||||
choices: []string{"Buy carrots", "Buy celery", "Buy kohlrabi"},
|
||||
|
||||
// A map which indicates which choices are selected. We're using
|
||||
// the map like a mathematical set. The keys refer to the indexes
|
||||
// of the `choices` slice, above.
|
||||
selected: make(map[int]struct{}),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Next, we define the `Init` method. `Init` can return a `Cmd` that could perform
|
||||
some initial I/O. For now, we don't need to do any I/O, so for the command,
|
||||
we'll just return `nil`, which translates to "no command."
|
||||
|
||||
```go
|
||||
func (m model) Init() tea.Cmd {
|
||||
// Just return `nil`, which means "no I/O right now, please."
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### The Update Method
|
||||
|
||||
Next up is the update method. The update function is called when ”things
|
||||
happen.” Its job is to look at what has happened and return an updated model in
|
||||
response. It can also return a `Cmd` to make more things happen, but for now
|
||||
don't worry about that part.
|
||||
|
||||
In our case, when a user presses the down arrow, `Update`’s job is to notice
|
||||
that the down arrow was pressed and move the cursor accordingly (or not).
|
||||
|
||||
The “something happened” comes in the form of a `Msg`, which can be any type.
|
||||
Messages are the result of some I/O that took place, such as a keypress, timer
|
||||
tick, or a response from a server.
|
||||
|
||||
We usually figure out which type of `Msg` we received with a type switch, but
|
||||
you could also use a type assertion.
|
||||
|
||||
For now, we'll just deal with `tea.KeyMsg` messages, which are automatically
|
||||
sent to the update function when keys are pressed.
|
||||
|
||||
```go
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
|
||||
// Is it a key press?
|
||||
case tea.KeyMsg:
|
||||
|
||||
// Cool, what was the actual key pressed?
|
||||
switch msg.String() {
|
||||
|
||||
// These keys should exit the program.
|
||||
case "ctrl+c", "q":
|
||||
return m, tea.Quit
|
||||
|
||||
// The "up" and "k" keys move the cursor up
|
||||
case "up", "k":
|
||||
if m.cursor > 0 {
|
||||
m.cursor--
|
||||
}
|
||||
|
||||
// The "down" and "j" keys move the cursor down
|
||||
case "down", "j":
|
||||
if m.cursor < len(m.choices)-1 {
|
||||
m.cursor++
|
||||
}
|
||||
|
||||
// The "enter" key and the spacebar (a literal space) toggle
|
||||
// the selected state for the item that the cursor is pointing at.
|
||||
case "enter", " ":
|
||||
_, ok := m.selected[m.cursor]
|
||||
if ok {
|
||||
delete(m.selected, m.cursor)
|
||||
} else {
|
||||
m.selected[m.cursor] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the updated model to the Bubble Tea runtime for processing.
|
||||
// Note that we're not returning a command.
|
||||
return m, nil
|
||||
}
|
||||
```
|
||||
|
||||
You may have noticed that <kbd>ctrl+c</kbd> and <kbd>q</kbd> above return
|
||||
a `tea.Quit` command with the model. That’s a special command which instructs
|
||||
the Bubble Tea runtime to quit, exiting the program.
|
||||
|
||||
### The View Method
|
||||
|
||||
At last, it’s time to render our UI. Of all the methods, the view is the
|
||||
simplest. We look at the model in its current state and use it to return
|
||||
a `string`. That string is our UI!
|
||||
|
||||
Because the view describes the entire UI of your application, you don’t have to
|
||||
worry about redrawing logic and stuff like that. Bubble Tea takes care of it
|
||||
for you.
|
||||
|
||||
```go
|
||||
func (m model) View() string {
|
||||
// The header
|
||||
s := "What should we buy at the market?\n\n"
|
||||
|
||||
// Iterate over our choices
|
||||
for i, choice := range m.choices {
|
||||
|
||||
// Is the cursor pointing at this choice?
|
||||
cursor := " " // no cursor
|
||||
if m.cursor == i {
|
||||
cursor = ">" // cursor!
|
||||
}
|
||||
|
||||
// Is this choice selected?
|
||||
checked := " " // not selected
|
||||
if _, ok := m.selected[i]; ok {
|
||||
checked = "x" // selected!
|
||||
}
|
||||
|
||||
// Render the row
|
||||
s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice)
|
||||
}
|
||||
|
||||
// The footer
|
||||
s += "\nPress q to quit.\n"
|
||||
|
||||
// Send the UI for rendering
|
||||
return s
|
||||
}
|
||||
```
|
||||
|
||||
### All Together Now
|
||||
|
||||
The last step is to simply run our program. We pass our initial model to
|
||||
`tea.NewProgram` and let it rip:
|
||||
|
||||
```go
|
||||
func main() {
|
||||
p := tea.NewProgram(initialModel())
|
||||
if _, err := p.Run(); err != nil {
|
||||
fmt.Printf("Alas, there's been an error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## What’s Next?
|
||||
|
||||
This tutorial covers the basics of building an interactive terminal UI, but
|
||||
in the real world you'll also need to perform I/O. To learn about that have a
|
||||
look at the [Command Tutorial][cmd]. It's pretty simple.
|
||||
|
||||
There are also several [Bubble Tea examples][examples] available and, of course,
|
||||
there are [Go Docs][docs].
|
||||
|
||||
[cmd]: https://github.com/charmbracelet/bubbletea/tree/main/tutorials/commands/
|
||||
[examples]: https://github.com/charmbracelet/bubbletea/tree/main/examples
|
||||
[docs]: https://pkg.go.dev/github.com/charmbracelet/bubbletea?tab=doc
|
||||
|
||||
## Debugging
|
||||
|
||||
### Debugging with Delve
|
||||
|
||||
Since Bubble Tea apps assume control of stdin and stdout, you’ll need to run
|
||||
delve in headless mode and then connect to it:
|
||||
|
||||
```bash
|
||||
# Start the debugger
|
||||
$ dlv debug --headless --api-version=2 --listen=127.0.0.1:43000 .
|
||||
API server listening at: 127.0.0.1:43000
|
||||
|
||||
# Connect to it from another terminal
|
||||
$ dlv connect 127.0.0.1:43000
|
||||
```
|
||||
|
||||
If you do not explicitly supply the `--listen` flag, the port used will vary
|
||||
per run, so passing this in makes the debugger easier to use from a script
|
||||
or your IDE of choice.
|
||||
|
||||
Additionally, we pass in `--api-version=2` because delve defaults to version 1
|
||||
for backwards compatibility reasons. However, delve recommends using version 2
|
||||
for all new development and some clients may no longer work with version 1.
|
||||
For more information, see the [Delve documentation](https://github.com/go-delve/delve/tree/master/Documentation/api).
|
||||
|
||||
### Logging Stuff
|
||||
|
||||
You can’t really log to stdout with Bubble Tea because your TUI is busy
|
||||
occupying that! You can, however, log to a file by including something like
|
||||
the following prior to starting your Bubble Tea program:
|
||||
|
||||
```go
|
||||
if len(os.Getenv("DEBUG")) > 0 {
|
||||
f, err := tea.LogToFile("debug.log", "debug")
|
||||
if err != nil {
|
||||
fmt.Println("fatal:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer f.Close()
|
||||
}
|
||||
```
|
||||
|
||||
To see what’s being logged in real time, run `tail -f debug.log` while you run
|
||||
your program in another window.
|
||||
|
||||
## Libraries we use with Bubble Tea
|
||||
|
||||
- [Bubbles][bubbles]: Common Bubble Tea components such as text inputs, viewports, spinners and so on
|
||||
- [Lip Gloss][lipgloss]: Style, format and layout tools for terminal applications
|
||||
- [Harmonica][harmonica]: A spring animation library for smooth, natural motion
|
||||
- [BubbleZone][bubblezone]: Easy mouse event tracking for Bubble Tea components
|
||||
- [ntcharts][ntcharts]: A terminal charting library built for Bubble Tea and [Lip Gloss][lipgloss]
|
||||
|
||||
[bubbles]: https://github.com/charmbracelet/bubbles
|
||||
[lipgloss]: https://github.com/charmbracelet/lipgloss
|
||||
[harmonica]: https://github.com/charmbracelet/harmonica
|
||||
[bubblezone]: https://github.com/lrstanley/bubblezone
|
||||
[ntcharts]: https://github.com/NimbleMarkets/ntcharts
|
||||
|
||||
## Bubble Tea in the Wild
|
||||
|
||||
There are over [10,000 applications](https://github.com/charmbracelet/bubbletea/network/dependents) built with Bubble Tea! Here are a handful of ’em.
|
||||
|
||||
### Staff favourites
|
||||
|
||||
- [chezmoi](https://github.com/twpayne/chezmoi): securely manage your dotfiles across multiple machines
|
||||
- [circumflex](https://github.com/bensadeh/circumflex): read Hacker News in the terminal
|
||||
- [gh-dash](https://www.github.com/dlvhdr/gh-dash): a GitHub CLI extension for PRs and issues
|
||||
- [Tetrigo](https://github.com/Broderick-Westrope/tetrigo): Tetris in the terminal
|
||||
- [Signls](https://github.com/emprcl/signls): a generative midi sequencer designed for composition and live performance
|
||||
- [Superfile](https://github.com/yorukot/superfile): a super file manager
|
||||
|
||||
### In Industry
|
||||
|
||||
- Microsoft Azure – [Aztify](https://github.com/Azure/aztfy): bring Microsoft Azure resources under Terraform
|
||||
- Daytona – [Daytona](https://github.com/daytonaio/daytona): open source dev environment manager
|
||||
- Cockroach Labs – [CockroachDB](https://github.com/cockroachdb/cockroach): a cloud-native, high-availability distributed SQL database
|
||||
- Truffle Security Co. – [Trufflehog](https://github.com/trufflesecurity/trufflehog): find leaked credentials
|
||||
- NVIDIA – [container-canary](https://github.com/NVIDIA/container-canary): a container validator
|
||||
- AWS – [eks-node-viewer](https://github.com/awslabs/eks-node-viewer): a tool for visualizing dynamic node usage within an EKS cluster
|
||||
- MinIO – [mc](https://github.com/minio/mc): the official [MinIO](https://min.io) client
|
||||
- Ubuntu – [Authd](https://github.com/ubuntu/authd): an authentication daemon for cloud-based identity providers
|
||||
|
||||
### Charm stuff
|
||||
|
||||
- [Glow](https://github.com/charmbracelet/glow): a markdown reader, browser, and online markdown stash
|
||||
- [Huh?](https://github.com/charmbracelet/huh): an interactive prompt and form toolkit
|
||||
- [Mods](https://github.com/charmbracelet/mods): AI on the CLI, built for pipelines
|
||||
- [Wishlist](https://github.com/charmbracelet/wishlist): an SSH directory (and bastion!)
|
||||
|
||||
### There’s so much more where that came from
|
||||
|
||||
For more applications built with Bubble Tea see [Charm & Friends][community].
|
||||
Is there something cool you made with Bubble Tea you want to share? [PRs][community] are
|
||||
welcome!
|
||||
|
||||
## Contributing
|
||||
|
||||
See [contributing][contribute].
|
||||
|
||||
[contribute]: https://github.com/charmbracelet/bubbletea/contribute
|
||||
|
||||
## Feedback
|
||||
|
||||
We’d love to hear your thoughts on this project. Feel free to drop us a note!
|
||||
|
||||
- [Twitter](https://twitter.com/charmcli)
|
||||
- [The Fediverse](https://mastodon.social/@charmcli)
|
||||
- [Discord](https://charm.sh/chat)
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
Bubble Tea is based on the paradigms of [The Elm Architecture][elm] by Evan
|
||||
Czaplicki et alia and the excellent [go-tea][gotea] by TJ Holowaychuk. It’s
|
||||
inspired by the many great [_Zeichenorientierte Benutzerschnittstellen_][zb]
|
||||
of days past.
|
||||
|
||||
[elm]: https://guide.elm-lang.org/architecture/
|
||||
[gotea]: https://github.com/tj/go-tea
|
||||
[zb]: https://de.wikipedia.org/wiki/Zeichenorientierte_Benutzerschnittstelle
|
||||
[community]: https://github.com/charm-and-friends/charm-in-the-wild
|
||||
|
||||
## License
|
||||
|
||||
[MIT](https://github.com/charmbracelet/bubbletea/raw/main/LICENSE)
|
||||
|
||||
---
|
||||
|
||||
Part of [Charm](https://charm.sh).
|
||||
|
||||
<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge.jpg" width="400"></a>
|
||||
|
||||
Charm热爱开源 • Charm loves open source • نحنُ نحب المصادر المفتوحة
|
216
vendor/github.com/charmbracelet/bubbletea/commands.go
generated
vendored
Normal file
216
vendor/github.com/charmbracelet/bubbletea/commands.go
generated
vendored
Normal file
@ -0,0 +1,216 @@
|
||||
package tea
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Batch performs a bunch of commands concurrently with no ordering guarantees
|
||||
// about the results. Use a Batch to return several commands.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func (m model) Init() Cmd {
|
||||
// return tea.Batch(someCommand, someOtherCommand)
|
||||
// }
|
||||
func Batch(cmds ...Cmd) Cmd {
|
||||
var validCmds []Cmd //nolint:prealloc
|
||||
for _, c := range cmds {
|
||||
if c == nil {
|
||||
continue
|
||||
}
|
||||
validCmds = append(validCmds, c)
|
||||
}
|
||||
switch len(validCmds) {
|
||||
case 0:
|
||||
return nil
|
||||
case 1:
|
||||
return validCmds[0]
|
||||
default:
|
||||
return func() Msg {
|
||||
return BatchMsg(validCmds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BatchMsg is a message used to perform a bunch of commands concurrently with
|
||||
// no ordering guarantees. You can send a BatchMsg with Batch.
|
||||
type BatchMsg []Cmd
|
||||
|
||||
// Sequence runs the given commands one at a time, in order. Contrast this with
|
||||
// Batch, which runs commands concurrently.
|
||||
func Sequence(cmds ...Cmd) Cmd {
|
||||
return func() Msg {
|
||||
return sequenceMsg(cmds)
|
||||
}
|
||||
}
|
||||
|
||||
// sequenceMsg is used internally to run the given commands in order.
|
||||
type sequenceMsg []Cmd
|
||||
|
||||
// Every is a command that ticks in sync with the system clock. So, if you
|
||||
// wanted to tick with the system clock every second, minute or hour you
|
||||
// could use this. It's also handy for having different things tick in sync.
|
||||
//
|
||||
// Because we're ticking with the system clock the tick will likely not run for
|
||||
// the entire specified duration. For example, if we're ticking for one minute
|
||||
// and the clock is at 12:34:20 then the next tick will happen at 12:35:00, 40
|
||||
// seconds later.
|
||||
//
|
||||
// To produce the command, pass a duration and a function which returns
|
||||
// a message containing the time at which the tick occurred.
|
||||
//
|
||||
// type TickMsg time.Time
|
||||
//
|
||||
// cmd := Every(time.Second, func(t time.Time) Msg {
|
||||
// return TickMsg(t)
|
||||
// })
|
||||
//
|
||||
// Beginners' note: Every sends a single message and won't automatically
|
||||
// dispatch messages at an interval. To do that, you'll want to return another
|
||||
// Every command after receiving your tick message. For example:
|
||||
//
|
||||
// type TickMsg time.Time
|
||||
//
|
||||
// // Send a message every second.
|
||||
// func tickEvery() Cmd {
|
||||
// return Every(time.Second, func(t time.Time) Msg {
|
||||
// return TickMsg(t)
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// func (m model) Init() Cmd {
|
||||
// // Start ticking.
|
||||
// return tickEvery()
|
||||
// }
|
||||
//
|
||||
// func (m model) Update(msg Msg) (Model, Cmd) {
|
||||
// switch msg.(type) {
|
||||
// case TickMsg:
|
||||
// // Return your Every command again to loop.
|
||||
// return m, tickEvery()
|
||||
// }
|
||||
// return m, nil
|
||||
// }
|
||||
//
|
||||
// Every is analogous to Tick in the Elm Architecture.
|
||||
func Every(duration time.Duration, fn func(time.Time) Msg) Cmd {
|
||||
n := time.Now()
|
||||
d := n.Truncate(duration).Add(duration).Sub(n)
|
||||
t := time.NewTimer(d)
|
||||
return func() Msg {
|
||||
ts := <-t.C
|
||||
t.Stop()
|
||||
for len(t.C) > 0 {
|
||||
<-t.C
|
||||
}
|
||||
return fn(ts)
|
||||
}
|
||||
}
|
||||
|
||||
// Tick produces a command at an interval independent of the system clock at
|
||||
// the given duration. That is, the timer begins precisely when invoked,
|
||||
// and runs for its entire duration.
|
||||
//
|
||||
// To produce the command, pass a duration and a function which returns
|
||||
// a message containing the time at which the tick occurred.
|
||||
//
|
||||
// type TickMsg time.Time
|
||||
//
|
||||
// cmd := Tick(time.Second, func(t time.Time) Msg {
|
||||
// return TickMsg(t)
|
||||
// })
|
||||
//
|
||||
// Beginners' note: Tick sends a single message and won't automatically
|
||||
// dispatch messages at an interval. To do that, you'll want to return another
|
||||
// Tick command after receiving your tick message. For example:
|
||||
//
|
||||
// type TickMsg time.Time
|
||||
//
|
||||
// func doTick() Cmd {
|
||||
// return Tick(time.Second, func(t time.Time) Msg {
|
||||
// return TickMsg(t)
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// func (m model) Init() Cmd {
|
||||
// // Start ticking.
|
||||
// return doTick()
|
||||
// }
|
||||
//
|
||||
// func (m model) Update(msg Msg) (Model, Cmd) {
|
||||
// switch msg.(type) {
|
||||
// case TickMsg:
|
||||
// // Return your Tick command again to loop.
|
||||
// return m, doTick()
|
||||
// }
|
||||
// return m, nil
|
||||
// }
|
||||
func Tick(d time.Duration, fn func(time.Time) Msg) Cmd {
|
||||
t := time.NewTimer(d)
|
||||
return func() Msg {
|
||||
ts := <-t.C
|
||||
t.Stop()
|
||||
for len(t.C) > 0 {
|
||||
<-t.C
|
||||
}
|
||||
return fn(ts)
|
||||
}
|
||||
}
|
||||
|
||||
// Sequentially produces a command that sequentially executes the given
|
||||
// commands.
|
||||
// The Msg returned is the first non-nil message returned by a Cmd.
|
||||
//
|
||||
// func saveStateCmd() Msg {
|
||||
// if err := save(); err != nil {
|
||||
// return errMsg{err}
|
||||
// }
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// cmd := Sequentially(saveStateCmd, Quit)
|
||||
//
|
||||
// Deprecated: use Sequence instead.
|
||||
func Sequentially(cmds ...Cmd) Cmd {
|
||||
return func() Msg {
|
||||
for _, cmd := range cmds {
|
||||
if cmd == nil {
|
||||
continue
|
||||
}
|
||||
if msg := cmd(); msg != nil {
|
||||
return msg
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// setWindowTitleMsg is an internal message used to set the window title.
|
||||
type setWindowTitleMsg string
|
||||
|
||||
// SetWindowTitle produces a command that sets the terminal title.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// func (m model) Init() Cmd {
|
||||
// // Set title.
|
||||
// return tea.SetWindowTitle("My App")
|
||||
// }
|
||||
func SetWindowTitle(title string) Cmd {
|
||||
return func() Msg {
|
||||
return setWindowTitleMsg(title)
|
||||
}
|
||||
}
|
||||
|
||||
type windowSizeMsg struct{}
|
||||
|
||||
// WindowSize is a command that queries the terminal for its current size. It
|
||||
// delivers the results to Update via a [WindowSizeMsg]. Keep in mind that
|
||||
// WindowSizeMsgs will automatically be delivered to Update when the [Program]
|
||||
// starts and when the window dimensions change so in many cases you will not
|
||||
// need to explicitly invoke this command.
|
||||
func WindowSize() Cmd {
|
||||
return func() Msg {
|
||||
return windowSizeMsg{}
|
||||
}
|
||||
}
|
129
vendor/github.com/charmbracelet/bubbletea/exec.go
generated
vendored
Normal file
129
vendor/github.com/charmbracelet/bubbletea/exec.go
generated
vendored
Normal file
@ -0,0 +1,129 @@
|
||||
package tea
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// execMsg is used internally to run an ExecCommand sent with Exec.
|
||||
type execMsg struct {
|
||||
cmd ExecCommand
|
||||
fn ExecCallback
|
||||
}
|
||||
|
||||
// Exec is used to perform arbitrary I/O in a blocking fashion, effectively
|
||||
// pausing the Program while execution is running and resuming it when
|
||||
// execution has completed.
|
||||
//
|
||||
// Most of the time you'll want to use ExecProcess, which runs an exec.Cmd.
|
||||
//
|
||||
// For non-interactive i/o you should use a Cmd (that is, a tea.Cmd).
|
||||
func Exec(c ExecCommand, fn ExecCallback) Cmd {
|
||||
return func() Msg {
|
||||
return execMsg{cmd: c, fn: fn}
|
||||
}
|
||||
}
|
||||
|
||||
// ExecProcess runs the given *exec.Cmd in a blocking fashion, effectively
|
||||
// pausing the Program while the command is running. After the *exec.Cmd exists
|
||||
// the Program resumes. It's useful for spawning other interactive applications
|
||||
// such as editors and shells from within a Program.
|
||||
//
|
||||
// To produce the command, pass an *exec.Cmd and a function which returns
|
||||
// a message containing the error which may have occurred when running the
|
||||
// ExecCommand.
|
||||
//
|
||||
// type VimFinishedMsg struct { err error }
|
||||
//
|
||||
// c := exec.Command("vim", "file.txt")
|
||||
//
|
||||
// cmd := ExecProcess(c, func(err error) Msg {
|
||||
// return VimFinishedMsg{err: err}
|
||||
// })
|
||||
//
|
||||
// Or, if you don't care about errors, you could simply:
|
||||
//
|
||||
// cmd := ExecProcess(exec.Command("vim", "file.txt"), nil)
|
||||
//
|
||||
// For non-interactive i/o you should use a Cmd (that is, a tea.Cmd).
|
||||
func ExecProcess(c *exec.Cmd, fn ExecCallback) Cmd {
|
||||
return Exec(wrapExecCommand(c), fn)
|
||||
}
|
||||
|
||||
// ExecCallback is used when executing an *exec.Command to return a message
|
||||
// with an error, which may or may not be nil.
|
||||
type ExecCallback func(error) Msg
|
||||
|
||||
// ExecCommand can be implemented to execute things in a blocking fashion in
|
||||
// the current terminal.
|
||||
type ExecCommand interface {
|
||||
Run() error
|
||||
SetStdin(io.Reader)
|
||||
SetStdout(io.Writer)
|
||||
SetStderr(io.Writer)
|
||||
}
|
||||
|
||||
// wrapExecCommand wraps an exec.Cmd so that it satisfies the ExecCommand
|
||||
// interface so it can be used with Exec.
|
||||
func wrapExecCommand(c *exec.Cmd) ExecCommand {
|
||||
return &osExecCommand{Cmd: c}
|
||||
}
|
||||
|
||||
// osExecCommand is a layer over an exec.Cmd that satisfies the ExecCommand
|
||||
// interface.
|
||||
type osExecCommand struct{ *exec.Cmd }
|
||||
|
||||
// SetStdin sets stdin on underlying exec.Cmd to the given io.Reader.
|
||||
func (c *osExecCommand) SetStdin(r io.Reader) {
|
||||
// If unset, have the command use the same input as the terminal.
|
||||
if c.Stdin == nil {
|
||||
c.Stdin = r
|
||||
}
|
||||
}
|
||||
|
||||
// SetStdout sets stdout on underlying exec.Cmd to the given io.Writer.
|
||||
func (c *osExecCommand) SetStdout(w io.Writer) {
|
||||
// If unset, have the command use the same output as the terminal.
|
||||
if c.Stdout == nil {
|
||||
c.Stdout = w
|
||||
}
|
||||
}
|
||||
|
||||
// SetStderr sets stderr on the underlying exec.Cmd to the given io.Writer.
|
||||
func (c *osExecCommand) SetStderr(w io.Writer) {
|
||||
// If unset, use stderr for the command's stderr
|
||||
if c.Stderr == nil {
|
||||
c.Stderr = w
|
||||
}
|
||||
}
|
||||
|
||||
// exec runs an ExecCommand and delivers the results to the program as a Msg.
|
||||
func (p *Program) exec(c ExecCommand, fn ExecCallback) {
|
||||
if err := p.ReleaseTerminal(); err != nil {
|
||||
// If we can't release input, abort.
|
||||
if fn != nil {
|
||||
go p.Send(fn(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
c.SetStdin(p.input)
|
||||
c.SetStdout(p.output)
|
||||
c.SetStderr(os.Stderr)
|
||||
|
||||
// Execute system command.
|
||||
if err := c.Run(); err != nil {
|
||||
_ = p.RestoreTerminal() // also try to restore the terminal.
|
||||
if fn != nil {
|
||||
go p.Send(fn(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Have the program re-capture input.
|
||||
err := p.RestoreTerminal()
|
||||
if fn != nil {
|
||||
go p.Send(fn(err))
|
||||
}
|
||||
}
|
9
vendor/github.com/charmbracelet/bubbletea/focus.go
generated
vendored
Normal file
9
vendor/github.com/charmbracelet/bubbletea/focus.go
generated
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
package tea
|
||||
|
||||
// FocusMsg represents a terminal focus message.
|
||||
// This occurs when the terminal gains focus.
|
||||
type FocusMsg struct{}
|
||||
|
||||
// BlurMsg represents a terminal blur message.
|
||||
// This occurs when the terminal loses focus.
|
||||
type BlurMsg struct{}
|
14
vendor/github.com/charmbracelet/bubbletea/inputreader_other.go
generated
vendored
Normal file
14
vendor/github.com/charmbracelet/bubbletea/inputreader_other.go
generated
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package tea
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/muesli/cancelreader"
|
||||
)
|
||||
|
||||
func newInputReader(r io.Reader, _ bool) (cancelreader.CancelReader, error) {
|
||||
return cancelreader.NewReader(r)
|
||||
}
|
127
vendor/github.com/charmbracelet/bubbletea/inputreader_windows.go
generated
vendored
Normal file
127
vendor/github.com/charmbracelet/bubbletea/inputreader_windows.go
generated
vendored
Normal file
@ -0,0 +1,127 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package tea
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/charmbracelet/x/term"
|
||||
"github.com/erikgeiser/coninput"
|
||||
"github.com/muesli/cancelreader"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
type conInputReader struct {
|
||||
cancelMixin
|
||||
|
||||
conin windows.Handle
|
||||
|
||||
originalMode uint32
|
||||
}
|
||||
|
||||
var _ cancelreader.CancelReader = &conInputReader{}
|
||||
|
||||
func newInputReader(r io.Reader, enableMouse bool) (cancelreader.CancelReader, error) {
|
||||
fallback := func(io.Reader) (cancelreader.CancelReader, error) {
|
||||
return cancelreader.NewReader(r)
|
||||
}
|
||||
if f, ok := r.(term.File); !ok || f.Fd() != os.Stdin.Fd() {
|
||||
return fallback(r)
|
||||
}
|
||||
|
||||
conin, err := coninput.NewStdinHandle()
|
||||
if err != nil {
|
||||
return fallback(r)
|
||||
}
|
||||
|
||||
modes := []uint32{
|
||||
windows.ENABLE_WINDOW_INPUT,
|
||||
windows.ENABLE_EXTENDED_FLAGS,
|
||||
}
|
||||
|
||||
// Since we have options to enable mouse events, [WithMouseCellMotion],
|
||||
// [WithMouseAllMotion], and [EnableMouseCellMotion],
|
||||
// [EnableMouseAllMotion], and [DisableMouse], we need to check if the user
|
||||
// has enabled mouse events and add the appropriate mode accordingly.
|
||||
// Otherwise, mouse events will be enabled all the time.
|
||||
if enableMouse {
|
||||
modes = append(modes, windows.ENABLE_MOUSE_INPUT)
|
||||
}
|
||||
|
||||
originalMode, err := prepareConsole(conin, modes...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to prepare console input: %w", err)
|
||||
}
|
||||
|
||||
return &conInputReader{
|
||||
conin: conin,
|
||||
originalMode: originalMode,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Cancel implements cancelreader.CancelReader.
|
||||
func (r *conInputReader) Cancel() bool {
|
||||
r.setCanceled()
|
||||
|
||||
return windows.CancelIoEx(r.conin, nil) == nil || windows.CancelIo(r.conin) == nil
|
||||
}
|
||||
|
||||
// Close implements cancelreader.CancelReader.
|
||||
func (r *conInputReader) Close() error {
|
||||
if r.originalMode != 0 {
|
||||
err := windows.SetConsoleMode(r.conin, r.originalMode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reset console mode: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read implements cancelreader.CancelReader.
|
||||
func (r *conInputReader) Read(_ []byte) (n int, err error) {
|
||||
if r.isCanceled() {
|
||||
err = cancelreader.ErrCanceled
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func prepareConsole(input windows.Handle, modes ...uint32) (originalMode uint32, err error) {
|
||||
err = windows.GetConsoleMode(input, &originalMode)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("get console mode: %w", err)
|
||||
}
|
||||
|
||||
newMode := coninput.AddInputModes(0, modes...)
|
||||
|
||||
err = windows.SetConsoleMode(input, newMode)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("set console mode: %w", err)
|
||||
}
|
||||
|
||||
return originalMode, nil
|
||||
}
|
||||
|
||||
// cancelMixin represents a goroutine-safe cancelation status.
|
||||
type cancelMixin struct {
|
||||
unsafeCanceled bool
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func (c *cancelMixin) setCanceled() {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
c.unsafeCanceled = true
|
||||
}
|
||||
|
||||
func (c *cancelMixin) isCanceled() bool {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
return c.unsafeCanceled
|
||||
}
|
715
vendor/github.com/charmbracelet/bubbletea/key.go
generated
vendored
Normal file
715
vendor/github.com/charmbracelet/bubbletea/key.go
generated
vendored
Normal file
@ -0,0 +1,715 @@
|
||||
package tea
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// KeyMsg contains information about a keypress. KeyMsgs are always sent to
|
||||
// the program's update function. There are a couple general patterns you could
|
||||
// use to check for keypresses:
|
||||
//
|
||||
// // Switch on the string representation of the key (shorter)
|
||||
// switch msg := msg.(type) {
|
||||
// case KeyMsg:
|
||||
// switch msg.String() {
|
||||
// case "enter":
|
||||
// fmt.Println("you pressed enter!")
|
||||
// case "a":
|
||||
// fmt.Println("you pressed a!")
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Switch on the key type (more foolproof)
|
||||
// switch msg := msg.(type) {
|
||||
// case KeyMsg:
|
||||
// switch msg.Type {
|
||||
// case KeyEnter:
|
||||
// fmt.Println("you pressed enter!")
|
||||
// case KeyRunes:
|
||||
// switch string(msg.Runes) {
|
||||
// case "a":
|
||||
// fmt.Println("you pressed a!")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Note that Key.Runes will always contain at least one character, so you can
|
||||
// always safely call Key.Runes[0]. In most cases Key.Runes will only contain
|
||||
// one character, though certain input method editors (most notably Chinese
|
||||
// IMEs) can input multiple runes at once.
|
||||
type KeyMsg Key
|
||||
|
||||
// String returns a string representation for a key message. It's safe (and
|
||||
// encouraged) for use in key comparison.
|
||||
func (k KeyMsg) String() (str string) {
|
||||
return Key(k).String()
|
||||
}
|
||||
|
||||
// Key contains information about a keypress.
|
||||
type Key struct {
|
||||
Type KeyType
|
||||
Runes []rune
|
||||
Alt bool
|
||||
Paste bool
|
||||
}
|
||||
|
||||
// String returns a friendly string representation for a key. It's safe (and
|
||||
// encouraged) for use in key comparison.
|
||||
//
|
||||
// k := Key{Type: KeyEnter}
|
||||
// fmt.Println(k)
|
||||
// // Output: enter
|
||||
func (k Key) String() (str string) {
|
||||
var buf strings.Builder
|
||||
if k.Alt {
|
||||
buf.WriteString("alt+")
|
||||
}
|
||||
if k.Type == KeyRunes {
|
||||
if k.Paste {
|
||||
// Note: bubbles/keys bindings currently do string compares to
|
||||
// recognize shortcuts. Since pasted text should never activate
|
||||
// shortcuts, we need to ensure that the binding code doesn't
|
||||
// match Key events that result from pastes. We achieve this
|
||||
// here by enclosing pastes in '[...]' so that the string
|
||||
// comparison in Matches() fails in that case.
|
||||
buf.WriteByte('[')
|
||||
}
|
||||
buf.WriteString(string(k.Runes))
|
||||
if k.Paste {
|
||||
buf.WriteByte(']')
|
||||
}
|
||||
return buf.String()
|
||||
} else if s, ok := keyNames[k.Type]; ok {
|
||||
buf.WriteString(s)
|
||||
return buf.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// KeyType indicates the key pressed, such as KeyEnter or KeyBreak or KeyCtrlC.
|
||||
// All other keys will be type KeyRunes. To get the rune value, check the Rune
|
||||
// method on a Key struct, or use the Key.String() method:
|
||||
//
|
||||
// k := Key{Type: KeyRunes, Runes: []rune{'a'}, Alt: true}
|
||||
// if k.Type == KeyRunes {
|
||||
//
|
||||
// fmt.Println(k.Runes)
|
||||
// // Output: a
|
||||
//
|
||||
// fmt.Println(k.String())
|
||||
// // Output: alt+a
|
||||
//
|
||||
// }
|
||||
type KeyType int
|
||||
|
||||
func (k KeyType) String() (str string) {
|
||||
if s, ok := keyNames[k]; ok {
|
||||
return s
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Control keys. We could do this with an iota, but the values are very
|
||||
// specific, so we set the values explicitly to avoid any confusion.
|
||||
//
|
||||
// See also:
|
||||
// https://en.wikipedia.org/wiki/C0_and_C1_control_codes
|
||||
const (
|
||||
keyNUL KeyType = 0 // null, \0
|
||||
keySOH KeyType = 1 // start of heading
|
||||
keySTX KeyType = 2 // start of text
|
||||
keyETX KeyType = 3 // break, ctrl+c
|
||||
keyEOT KeyType = 4 // end of transmission
|
||||
keyENQ KeyType = 5 // enquiry
|
||||
keyACK KeyType = 6 // acknowledge
|
||||
keyBEL KeyType = 7 // bell, \a
|
||||
keyBS KeyType = 8 // backspace
|
||||
keyHT KeyType = 9 // horizontal tabulation, \t
|
||||
keyLF KeyType = 10 // line feed, \n
|
||||
keyVT KeyType = 11 // vertical tabulation \v
|
||||
keyFF KeyType = 12 // form feed \f
|
||||
keyCR KeyType = 13 // carriage return, \r
|
||||
keySO KeyType = 14 // shift out
|
||||
keySI KeyType = 15 // shift in
|
||||
keyDLE KeyType = 16 // data link escape
|
||||
keyDC1 KeyType = 17 // device control one
|
||||
keyDC2 KeyType = 18 // device control two
|
||||
keyDC3 KeyType = 19 // device control three
|
||||
keyDC4 KeyType = 20 // device control four
|
||||
keyNAK KeyType = 21 // negative acknowledge
|
||||
keySYN KeyType = 22 // synchronous idle
|
||||
keyETB KeyType = 23 // end of transmission block
|
||||
keyCAN KeyType = 24 // cancel
|
||||
keyEM KeyType = 25 // end of medium
|
||||
keySUB KeyType = 26 // substitution
|
||||
keyESC KeyType = 27 // escape, \e
|
||||
keyFS KeyType = 28 // file separator
|
||||
keyGS KeyType = 29 // group separator
|
||||
keyRS KeyType = 30 // record separator
|
||||
keyUS KeyType = 31 // unit separator
|
||||
keyDEL KeyType = 127 // delete. on most systems this is mapped to backspace, I hear
|
||||
)
|
||||
|
||||
// Control key aliases.
|
||||
const (
|
||||
KeyNull KeyType = keyNUL
|
||||
KeyBreak KeyType = keyETX
|
||||
KeyEnter KeyType = keyCR
|
||||
KeyBackspace KeyType = keyDEL
|
||||
KeyTab KeyType = keyHT
|
||||
KeyEsc KeyType = keyESC
|
||||
KeyEscape KeyType = keyESC
|
||||
|
||||
KeyCtrlAt KeyType = keyNUL // ctrl+@
|
||||
KeyCtrlA KeyType = keySOH
|
||||
KeyCtrlB KeyType = keySTX
|
||||
KeyCtrlC KeyType = keyETX
|
||||
KeyCtrlD KeyType = keyEOT
|
||||
KeyCtrlE KeyType = keyENQ
|
||||
KeyCtrlF KeyType = keyACK
|
||||
KeyCtrlG KeyType = keyBEL
|
||||
KeyCtrlH KeyType = keyBS
|
||||
KeyCtrlI KeyType = keyHT
|
||||
KeyCtrlJ KeyType = keyLF
|
||||
KeyCtrlK KeyType = keyVT
|
||||
KeyCtrlL KeyType = keyFF
|
||||
KeyCtrlM KeyType = keyCR
|
||||
KeyCtrlN KeyType = keySO
|
||||
KeyCtrlO KeyType = keySI
|
||||
KeyCtrlP KeyType = keyDLE
|
||||
KeyCtrlQ KeyType = keyDC1
|
||||
KeyCtrlR KeyType = keyDC2
|
||||
KeyCtrlS KeyType = keyDC3
|
||||
KeyCtrlT KeyType = keyDC4
|
||||
KeyCtrlU KeyType = keyNAK
|
||||
KeyCtrlV KeyType = keySYN
|
||||
KeyCtrlW KeyType = keyETB
|
||||
KeyCtrlX KeyType = keyCAN
|
||||
KeyCtrlY KeyType = keyEM
|
||||
KeyCtrlZ KeyType = keySUB
|
||||
KeyCtrlOpenBracket KeyType = keyESC // ctrl+[
|
||||
KeyCtrlBackslash KeyType = keyFS // ctrl+\
|
||||
KeyCtrlCloseBracket KeyType = keyGS // ctrl+]
|
||||
KeyCtrlCaret KeyType = keyRS // ctrl+^
|
||||
KeyCtrlUnderscore KeyType = keyUS // ctrl+_
|
||||
KeyCtrlQuestionMark KeyType = keyDEL // ctrl+?
|
||||
)
|
||||
|
||||
// Other keys.
|
||||
const (
|
||||
KeyRunes KeyType = -(iota + 1)
|
||||
KeyUp
|
||||
KeyDown
|
||||
KeyRight
|
||||
KeyLeft
|
||||
KeyShiftTab
|
||||
KeyHome
|
||||
KeyEnd
|
||||
KeyPgUp
|
||||
KeyPgDown
|
||||
KeyCtrlPgUp
|
||||
KeyCtrlPgDown
|
||||
KeyDelete
|
||||
KeyInsert
|
||||
KeySpace
|
||||
KeyCtrlUp
|
||||
KeyCtrlDown
|
||||
KeyCtrlRight
|
||||
KeyCtrlLeft
|
||||
KeyCtrlHome
|
||||
KeyCtrlEnd
|
||||
KeyShiftUp
|
||||
KeyShiftDown
|
||||
KeyShiftRight
|
||||
KeyShiftLeft
|
||||
KeyShiftHome
|
||||
KeyShiftEnd
|
||||
KeyCtrlShiftUp
|
||||
KeyCtrlShiftDown
|
||||
KeyCtrlShiftLeft
|
||||
KeyCtrlShiftRight
|
||||
KeyCtrlShiftHome
|
||||
KeyCtrlShiftEnd
|
||||
KeyF1
|
||||
KeyF2
|
||||
KeyF3
|
||||
KeyF4
|
||||
KeyF5
|
||||
KeyF6
|
||||
KeyF7
|
||||
KeyF8
|
||||
KeyF9
|
||||
KeyF10
|
||||
KeyF11
|
||||
KeyF12
|
||||
KeyF13
|
||||
KeyF14
|
||||
KeyF15
|
||||
KeyF16
|
||||
KeyF17
|
||||
KeyF18
|
||||
KeyF19
|
||||
KeyF20
|
||||
)
|
||||
|
||||
// Mappings for control keys and other special keys to friendly consts.
|
||||
var keyNames = map[KeyType]string{
|
||||
// Control keys.
|
||||
keyNUL: "ctrl+@", // also ctrl+` (that's ctrl+backtick)
|
||||
keySOH: "ctrl+a",
|
||||
keySTX: "ctrl+b",
|
||||
keyETX: "ctrl+c",
|
||||
keyEOT: "ctrl+d",
|
||||
keyENQ: "ctrl+e",
|
||||
keyACK: "ctrl+f",
|
||||
keyBEL: "ctrl+g",
|
||||
keyBS: "ctrl+h",
|
||||
keyHT: "tab", // also ctrl+i
|
||||
keyLF: "ctrl+j",
|
||||
keyVT: "ctrl+k",
|
||||
keyFF: "ctrl+l",
|
||||
keyCR: "enter",
|
||||
keySO: "ctrl+n",
|
||||
keySI: "ctrl+o",
|
||||
keyDLE: "ctrl+p",
|
||||
keyDC1: "ctrl+q",
|
||||
keyDC2: "ctrl+r",
|
||||
keyDC3: "ctrl+s",
|
||||
keyDC4: "ctrl+t",
|
||||
keyNAK: "ctrl+u",
|
||||
keySYN: "ctrl+v",
|
||||
keyETB: "ctrl+w",
|
||||
keyCAN: "ctrl+x",
|
||||
keyEM: "ctrl+y",
|
||||
keySUB: "ctrl+z",
|
||||
keyESC: "esc",
|
||||
keyFS: "ctrl+\\",
|
||||
keyGS: "ctrl+]",
|
||||
keyRS: "ctrl+^",
|
||||
keyUS: "ctrl+_",
|
||||
keyDEL: "backspace",
|
||||
|
||||
// Other keys.
|
||||
KeyRunes: "runes",
|
||||
KeyUp: "up",
|
||||
KeyDown: "down",
|
||||
KeyRight: "right",
|
||||
KeySpace: " ", // for backwards compatibility
|
||||
KeyLeft: "left",
|
||||
KeyShiftTab: "shift+tab",
|
||||
KeyHome: "home",
|
||||
KeyEnd: "end",
|
||||
KeyCtrlHome: "ctrl+home",
|
||||
KeyCtrlEnd: "ctrl+end",
|
||||
KeyShiftHome: "shift+home",
|
||||
KeyShiftEnd: "shift+end",
|
||||
KeyCtrlShiftHome: "ctrl+shift+home",
|
||||
KeyCtrlShiftEnd: "ctrl+shift+end",
|
||||
KeyPgUp: "pgup",
|
||||
KeyPgDown: "pgdown",
|
||||
KeyCtrlPgUp: "ctrl+pgup",
|
||||
KeyCtrlPgDown: "ctrl+pgdown",
|
||||
KeyDelete: "delete",
|
||||
KeyInsert: "insert",
|
||||
KeyCtrlUp: "ctrl+up",
|
||||
KeyCtrlDown: "ctrl+down",
|
||||
KeyCtrlRight: "ctrl+right",
|
||||
KeyCtrlLeft: "ctrl+left",
|
||||
KeyShiftUp: "shift+up",
|
||||
KeyShiftDown: "shift+down",
|
||||
KeyShiftRight: "shift+right",
|
||||
KeyShiftLeft: "shift+left",
|
||||
KeyCtrlShiftUp: "ctrl+shift+up",
|
||||
KeyCtrlShiftDown: "ctrl+shift+down",
|
||||
KeyCtrlShiftLeft: "ctrl+shift+left",
|
||||
KeyCtrlShiftRight: "ctrl+shift+right",
|
||||
KeyF1: "f1",
|
||||
KeyF2: "f2",
|
||||
KeyF3: "f3",
|
||||
KeyF4: "f4",
|
||||
KeyF5: "f5",
|
||||
KeyF6: "f6",
|
||||
KeyF7: "f7",
|
||||
KeyF8: "f8",
|
||||
KeyF9: "f9",
|
||||
KeyF10: "f10",
|
||||
KeyF11: "f11",
|
||||
KeyF12: "f12",
|
||||
KeyF13: "f13",
|
||||
KeyF14: "f14",
|
||||
KeyF15: "f15",
|
||||
KeyF16: "f16",
|
||||
KeyF17: "f17",
|
||||
KeyF18: "f18",
|
||||
KeyF19: "f19",
|
||||
KeyF20: "f20",
|
||||
}
|
||||
|
||||
// Sequence mappings.
|
||||
var sequences = map[string]Key{
|
||||
// Arrow keys
|
||||
"\x1b[A": {Type: KeyUp},
|
||||
"\x1b[B": {Type: KeyDown},
|
||||
"\x1b[C": {Type: KeyRight},
|
||||
"\x1b[D": {Type: KeyLeft},
|
||||
"\x1b[1;2A": {Type: KeyShiftUp},
|
||||
"\x1b[1;2B": {Type: KeyShiftDown},
|
||||
"\x1b[1;2C": {Type: KeyShiftRight},
|
||||
"\x1b[1;2D": {Type: KeyShiftLeft},
|
||||
"\x1b[OA": {Type: KeyShiftUp}, // DECCKM
|
||||
"\x1b[OB": {Type: KeyShiftDown}, // DECCKM
|
||||
"\x1b[OC": {Type: KeyShiftRight}, // DECCKM
|
||||
"\x1b[OD": {Type: KeyShiftLeft}, // DECCKM
|
||||
"\x1b[a": {Type: KeyShiftUp}, // urxvt
|
||||
"\x1b[b": {Type: KeyShiftDown}, // urxvt
|
||||
"\x1b[c": {Type: KeyShiftRight}, // urxvt
|
||||
"\x1b[d": {Type: KeyShiftLeft}, // urxvt
|
||||
"\x1b[1;3A": {Type: KeyUp, Alt: true},
|
||||
"\x1b[1;3B": {Type: KeyDown, Alt: true},
|
||||
"\x1b[1;3C": {Type: KeyRight, Alt: true},
|
||||
"\x1b[1;3D": {Type: KeyLeft, Alt: true},
|
||||
|
||||
"\x1b[1;4A": {Type: KeyShiftUp, Alt: true},
|
||||
"\x1b[1;4B": {Type: KeyShiftDown, Alt: true},
|
||||
"\x1b[1;4C": {Type: KeyShiftRight, Alt: true},
|
||||
"\x1b[1;4D": {Type: KeyShiftLeft, Alt: true},
|
||||
|
||||
"\x1b[1;5A": {Type: KeyCtrlUp},
|
||||
"\x1b[1;5B": {Type: KeyCtrlDown},
|
||||
"\x1b[1;5C": {Type: KeyCtrlRight},
|
||||
"\x1b[1;5D": {Type: KeyCtrlLeft},
|
||||
"\x1b[Oa": {Type: KeyCtrlUp, Alt: true}, // urxvt
|
||||
"\x1b[Ob": {Type: KeyCtrlDown, Alt: true}, // urxvt
|
||||
"\x1b[Oc": {Type: KeyCtrlRight, Alt: true}, // urxvt
|
||||
"\x1b[Od": {Type: KeyCtrlLeft, Alt: true}, // urxvt
|
||||
"\x1b[1;6A": {Type: KeyCtrlShiftUp},
|
||||
"\x1b[1;6B": {Type: KeyCtrlShiftDown},
|
||||
"\x1b[1;6C": {Type: KeyCtrlShiftRight},
|
||||
"\x1b[1;6D": {Type: KeyCtrlShiftLeft},
|
||||
"\x1b[1;7A": {Type: KeyCtrlUp, Alt: true},
|
||||
"\x1b[1;7B": {Type: KeyCtrlDown, Alt: true},
|
||||
"\x1b[1;7C": {Type: KeyCtrlRight, Alt: true},
|
||||
"\x1b[1;7D": {Type: KeyCtrlLeft, Alt: true},
|
||||
"\x1b[1;8A": {Type: KeyCtrlShiftUp, Alt: true},
|
||||
"\x1b[1;8B": {Type: KeyCtrlShiftDown, Alt: true},
|
||||
"\x1b[1;8C": {Type: KeyCtrlShiftRight, Alt: true},
|
||||
"\x1b[1;8D": {Type: KeyCtrlShiftLeft, Alt: true},
|
||||
|
||||
// Miscellaneous keys
|
||||
"\x1b[Z": {Type: KeyShiftTab},
|
||||
|
||||
"\x1b[2~": {Type: KeyInsert},
|
||||
"\x1b[3;2~": {Type: KeyInsert, Alt: true},
|
||||
|
||||
"\x1b[3~": {Type: KeyDelete},
|
||||
"\x1b[3;3~": {Type: KeyDelete, Alt: true},
|
||||
|
||||
"\x1b[5~": {Type: KeyPgUp},
|
||||
"\x1b[5;3~": {Type: KeyPgUp, Alt: true},
|
||||
"\x1b[5;5~": {Type: KeyCtrlPgUp},
|
||||
"\x1b[5^": {Type: KeyCtrlPgUp}, // urxvt
|
||||
"\x1b[5;7~": {Type: KeyCtrlPgUp, Alt: true},
|
||||
|
||||
"\x1b[6~": {Type: KeyPgDown},
|
||||
"\x1b[6;3~": {Type: KeyPgDown, Alt: true},
|
||||
"\x1b[6;5~": {Type: KeyCtrlPgDown},
|
||||
"\x1b[6^": {Type: KeyCtrlPgDown}, // urxvt
|
||||
"\x1b[6;7~": {Type: KeyCtrlPgDown, Alt: true},
|
||||
|
||||
"\x1b[1~": {Type: KeyHome},
|
||||
"\x1b[H": {Type: KeyHome}, // xterm, lxterm
|
||||
"\x1b[1;3H": {Type: KeyHome, Alt: true}, // xterm, lxterm
|
||||
"\x1b[1;5H": {Type: KeyCtrlHome}, // xterm, lxterm
|
||||
"\x1b[1;7H": {Type: KeyCtrlHome, Alt: true}, // xterm, lxterm
|
||||
"\x1b[1;2H": {Type: KeyShiftHome}, // xterm, lxterm
|
||||
"\x1b[1;4H": {Type: KeyShiftHome, Alt: true}, // xterm, lxterm
|
||||
"\x1b[1;6H": {Type: KeyCtrlShiftHome}, // xterm, lxterm
|
||||
"\x1b[1;8H": {Type: KeyCtrlShiftHome, Alt: true}, // xterm, lxterm
|
||||
|
||||
"\x1b[4~": {Type: KeyEnd},
|
||||
"\x1b[F": {Type: KeyEnd}, // xterm, lxterm
|
||||
"\x1b[1;3F": {Type: KeyEnd, Alt: true}, // xterm, lxterm
|
||||
"\x1b[1;5F": {Type: KeyCtrlEnd}, // xterm, lxterm
|
||||
"\x1b[1;7F": {Type: KeyCtrlEnd, Alt: true}, // xterm, lxterm
|
||||
"\x1b[1;2F": {Type: KeyShiftEnd}, // xterm, lxterm
|
||||
"\x1b[1;4F": {Type: KeyShiftEnd, Alt: true}, // xterm, lxterm
|
||||
"\x1b[1;6F": {Type: KeyCtrlShiftEnd}, // xterm, lxterm
|
||||
"\x1b[1;8F": {Type: KeyCtrlShiftEnd, Alt: true}, // xterm, lxterm
|
||||
|
||||
"\x1b[7~": {Type: KeyHome}, // urxvt
|
||||
"\x1b[7^": {Type: KeyCtrlHome}, // urxvt
|
||||
"\x1b[7$": {Type: KeyShiftHome}, // urxvt
|
||||
"\x1b[7@": {Type: KeyCtrlShiftHome}, // urxvt
|
||||
|
||||
"\x1b[8~": {Type: KeyEnd}, // urxvt
|
||||
"\x1b[8^": {Type: KeyCtrlEnd}, // urxvt
|
||||
"\x1b[8$": {Type: KeyShiftEnd}, // urxvt
|
||||
"\x1b[8@": {Type: KeyCtrlShiftEnd}, // urxvt
|
||||
|
||||
// Function keys, Linux console
|
||||
"\x1b[[A": {Type: KeyF1}, // linux console
|
||||
"\x1b[[B": {Type: KeyF2}, // linux console
|
||||
"\x1b[[C": {Type: KeyF3}, // linux console
|
||||
"\x1b[[D": {Type: KeyF4}, // linux console
|
||||
"\x1b[[E": {Type: KeyF5}, // linux console
|
||||
|
||||
// Function keys, X11
|
||||
"\x1bOP": {Type: KeyF1}, // vt100, xterm
|
||||
"\x1bOQ": {Type: KeyF2}, // vt100, xterm
|
||||
"\x1bOR": {Type: KeyF3}, // vt100, xterm
|
||||
"\x1bOS": {Type: KeyF4}, // vt100, xterm
|
||||
|
||||
"\x1b[1;3P": {Type: KeyF1, Alt: true}, // vt100, xterm
|
||||
"\x1b[1;3Q": {Type: KeyF2, Alt: true}, // vt100, xterm
|
||||
"\x1b[1;3R": {Type: KeyF3, Alt: true}, // vt100, xterm
|
||||
"\x1b[1;3S": {Type: KeyF4, Alt: true}, // vt100, xterm
|
||||
|
||||
"\x1b[11~": {Type: KeyF1}, // urxvt
|
||||
"\x1b[12~": {Type: KeyF2}, // urxvt
|
||||
"\x1b[13~": {Type: KeyF3}, // urxvt
|
||||
"\x1b[14~": {Type: KeyF4}, // urxvt
|
||||
|
||||
"\x1b[15~": {Type: KeyF5}, // vt100, xterm, also urxvt
|
||||
|
||||
"\x1b[15;3~": {Type: KeyF5, Alt: true}, // vt100, xterm, also urxvt
|
||||
|
||||
"\x1b[17~": {Type: KeyF6}, // vt100, xterm, also urxvt
|
||||
"\x1b[18~": {Type: KeyF7}, // vt100, xterm, also urxvt
|
||||
"\x1b[19~": {Type: KeyF8}, // vt100, xterm, also urxvt
|
||||
"\x1b[20~": {Type: KeyF9}, // vt100, xterm, also urxvt
|
||||
"\x1b[21~": {Type: KeyF10}, // vt100, xterm, also urxvt
|
||||
|
||||
"\x1b[17;3~": {Type: KeyF6, Alt: true}, // vt100, xterm
|
||||
"\x1b[18;3~": {Type: KeyF7, Alt: true}, // vt100, xterm
|
||||
"\x1b[19;3~": {Type: KeyF8, Alt: true}, // vt100, xterm
|
||||
"\x1b[20;3~": {Type: KeyF9, Alt: true}, // vt100, xterm
|
||||
"\x1b[21;3~": {Type: KeyF10, Alt: true}, // vt100, xterm
|
||||
|
||||
"\x1b[23~": {Type: KeyF11}, // vt100, xterm, also urxvt
|
||||
"\x1b[24~": {Type: KeyF12}, // vt100, xterm, also urxvt
|
||||
|
||||
"\x1b[23;3~": {Type: KeyF11, Alt: true}, // vt100, xterm
|
||||
"\x1b[24;3~": {Type: KeyF12, Alt: true}, // vt100, xterm
|
||||
|
||||
"\x1b[1;2P": {Type: KeyF13},
|
||||
"\x1b[1;2Q": {Type: KeyF14},
|
||||
|
||||
"\x1b[25~": {Type: KeyF13}, // vt100, xterm, also urxvt
|
||||
"\x1b[26~": {Type: KeyF14}, // vt100, xterm, also urxvt
|
||||
|
||||
"\x1b[25;3~": {Type: KeyF13, Alt: true}, // vt100, xterm
|
||||
"\x1b[26;3~": {Type: KeyF14, Alt: true}, // vt100, xterm
|
||||
|
||||
"\x1b[1;2R": {Type: KeyF15},
|
||||
"\x1b[1;2S": {Type: KeyF16},
|
||||
|
||||
"\x1b[28~": {Type: KeyF15}, // vt100, xterm, also urxvt
|
||||
"\x1b[29~": {Type: KeyF16}, // vt100, xterm, also urxvt
|
||||
|
||||
"\x1b[28;3~": {Type: KeyF15, Alt: true}, // vt100, xterm
|
||||
"\x1b[29;3~": {Type: KeyF16, Alt: true}, // vt100, xterm
|
||||
|
||||
"\x1b[15;2~": {Type: KeyF17},
|
||||
"\x1b[17;2~": {Type: KeyF18},
|
||||
"\x1b[18;2~": {Type: KeyF19},
|
||||
"\x1b[19;2~": {Type: KeyF20},
|
||||
|
||||
"\x1b[31~": {Type: KeyF17},
|
||||
"\x1b[32~": {Type: KeyF18},
|
||||
"\x1b[33~": {Type: KeyF19},
|
||||
"\x1b[34~": {Type: KeyF20},
|
||||
|
||||
// Powershell sequences.
|
||||
"\x1bOA": {Type: KeyUp, Alt: false},
|
||||
"\x1bOB": {Type: KeyDown, Alt: false},
|
||||
"\x1bOC": {Type: KeyRight, Alt: false},
|
||||
"\x1bOD": {Type: KeyLeft, Alt: false},
|
||||
}
|
||||
|
||||
// unknownInputByteMsg is reported by the input reader when an invalid
|
||||
// utf-8 byte is detected on the input. Currently, it is not handled
|
||||
// further by bubbletea. However, having this event makes it possible
|
||||
// to troubleshoot invalid inputs.
|
||||
type unknownInputByteMsg byte
|
||||
|
||||
func (u unknownInputByteMsg) String() string {
|
||||
return fmt.Sprintf("?%#02x?", int(u))
|
||||
}
|
||||
|
||||
// unknownCSISequenceMsg is reported by the input reader when an
|
||||
// unrecognized CSI sequence is detected on the input. Currently, it
|
||||
// is not handled further by bubbletea. However, having this event
|
||||
// makes it possible to troubleshoot invalid inputs.
|
||||
type unknownCSISequenceMsg []byte
|
||||
|
||||
func (u unknownCSISequenceMsg) String() string {
|
||||
return fmt.Sprintf("?CSI%+v?", []byte(u)[2:])
|
||||
}
|
||||
|
||||
var spaceRunes = []rune{' '}
|
||||
|
||||
// readAnsiInputs reads keypress and mouse inputs from a TTY and produces messages
|
||||
// containing information about the key or mouse events accordingly.
|
||||
func readAnsiInputs(ctx context.Context, msgs chan<- Msg, input io.Reader) error {
|
||||
var buf [256]byte
|
||||
|
||||
var leftOverFromPrevIteration []byte
|
||||
loop:
|
||||
for {
|
||||
// Read and block.
|
||||
numBytes, err := input.Read(buf[:])
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading input: %w", err)
|
||||
}
|
||||
b := buf[:numBytes]
|
||||
if leftOverFromPrevIteration != nil {
|
||||
b = append(leftOverFromPrevIteration, b...)
|
||||
}
|
||||
|
||||
// If we had a short read (numBytes < len(buf)), we're sure that
|
||||
// the end of this read is an event boundary, so there is no doubt
|
||||
// if we are encountering the end of the buffer while parsing a message.
|
||||
// However, if we've succeeded in filling up the buffer, there may
|
||||
// be more data in the OS buffer ready to be read in, to complete
|
||||
// the last message in the input. In that case, we will retry with
|
||||
// the left over data in the next iteration.
|
||||
canHaveMoreData := numBytes == len(buf)
|
||||
|
||||
var i, w int
|
||||
for i, w = 0, 0; i < len(b); i += w {
|
||||
var msg Msg
|
||||
w, msg = detectOneMsg(b[i:], canHaveMoreData)
|
||||
if w == 0 {
|
||||
// Expecting more bytes beyond the current buffer. Try waiting
|
||||
// for more input.
|
||||
leftOverFromPrevIteration = make([]byte, 0, len(b[i:])+len(buf))
|
||||
leftOverFromPrevIteration = append(leftOverFromPrevIteration, b[i:]...)
|
||||
continue loop
|
||||
}
|
||||
|
||||
select {
|
||||
case msgs <- msg:
|
||||
case <-ctx.Done():
|
||||
err := ctx.Err()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("found context error while reading input: %w", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
leftOverFromPrevIteration = nil
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
unknownCSIRe = regexp.MustCompile(`^\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]`)
|
||||
mouseSGRRegex = regexp.MustCompile(`(\d+);(\d+);(\d+)([Mm])`)
|
||||
)
|
||||
|
||||
func detectOneMsg(b []byte, canHaveMoreData bool) (w int, msg Msg) {
|
||||
// Detect mouse events.
|
||||
// X10 mouse events have a length of 6 bytes
|
||||
const mouseEventX10Len = 6
|
||||
if len(b) >= mouseEventX10Len && b[0] == '\x1b' && b[1] == '[' {
|
||||
switch b[2] {
|
||||
case 'M':
|
||||
return mouseEventX10Len, MouseMsg(parseX10MouseEvent(b))
|
||||
case '<':
|
||||
if matchIndices := mouseSGRRegex.FindSubmatchIndex(b[3:]); matchIndices != nil {
|
||||
// SGR mouse events length is the length of the match plus the length of the escape sequence
|
||||
mouseEventSGRLen := matchIndices[1] + 3 //nolint:gomnd
|
||||
return mouseEventSGRLen, MouseMsg(parseSGRMouseEvent(b))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Detect focus events.
|
||||
var foundRF bool
|
||||
foundRF, w, msg = detectReportFocus(b)
|
||||
if foundRF {
|
||||
return w, msg
|
||||
}
|
||||
|
||||
// Detect bracketed paste.
|
||||
var foundbp bool
|
||||
foundbp, w, msg = detectBracketedPaste(b)
|
||||
if foundbp {
|
||||
return w, msg
|
||||
}
|
||||
|
||||
// Detect escape sequence and control characters other than NUL,
|
||||
// possibly with an escape character in front to mark the Alt
|
||||
// modifier.
|
||||
var foundSeq bool
|
||||
foundSeq, w, msg = detectSequence(b)
|
||||
if foundSeq {
|
||||
return w, msg
|
||||
}
|
||||
|
||||
// No non-NUL control character or escape sequence.
|
||||
// If we are seeing at least an escape character, remember it for later below.
|
||||
alt := false
|
||||
i := 0
|
||||
if b[0] == '\x1b' {
|
||||
alt = true
|
||||
i++
|
||||
}
|
||||
|
||||
// Are we seeing a standalone NUL? This is not handled by detectSequence().
|
||||
if i < len(b) && b[i] == 0 {
|
||||
return i + 1, KeyMsg{Type: keyNUL, Alt: alt}
|
||||
}
|
||||
|
||||
// Find the longest sequence of runes that are not control
|
||||
// characters from this point.
|
||||
var runes []rune
|
||||
for rw := 0; i < len(b); i += rw {
|
||||
var r rune
|
||||
r, rw = utf8.DecodeRune(b[i:])
|
||||
if r == utf8.RuneError || r <= rune(keyUS) || r == rune(keyDEL) || r == ' ' {
|
||||
// Rune errors are handled below; control characters and spaces will
|
||||
// be handled by detectSequence in the next call to detectOneMsg.
|
||||
break
|
||||
}
|
||||
runes = append(runes, r)
|
||||
if alt {
|
||||
// We only support a single rune after an escape alt modifier.
|
||||
i += rw
|
||||
break
|
||||
}
|
||||
}
|
||||
if i >= len(b) && canHaveMoreData {
|
||||
// We have encountered the end of the input buffer. Alas, we can't
|
||||
// be sure whether the data in the remainder of the buffer is
|
||||
// complete (maybe there was a short read). Instead of sending anything
|
||||
// dumb to the message channel, do a short read. The outer loop will
|
||||
// handle this case by extending the buffer as necessary.
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// If we found at least one rune, we report the bunch of them as
|
||||
// a single KeyRunes or KeySpace event.
|
||||
if len(runes) > 0 {
|
||||
k := Key{Type: KeyRunes, Runes: runes, Alt: alt}
|
||||
if len(runes) == 1 && runes[0] == ' ' {
|
||||
k.Type = KeySpace
|
||||
}
|
||||
return i, KeyMsg(k)
|
||||
}
|
||||
|
||||
// We didn't find an escape sequence, nor a valid rune. Was this a
|
||||
// lone escape character at the end of the input?
|
||||
if alt && len(b) == 1 {
|
||||
return 1, KeyMsg(Key{Type: KeyEscape})
|
||||
}
|
||||
|
||||
// The character at the current position is neither an escape
|
||||
// sequence, a valid rune start or a sole escape character. Report
|
||||
// it as an invalid byte.
|
||||
return 1, unknownInputByteMsg(b[0])
|
||||
}
|
13
vendor/github.com/charmbracelet/bubbletea/key_other.go
generated
vendored
Normal file
13
vendor/github.com/charmbracelet/bubbletea/key_other.go
generated
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package tea
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
)
|
||||
|
||||
func readInputs(ctx context.Context, msgs chan<- Msg, input io.Reader) error {
|
||||
return readAnsiInputs(ctx, msgs, input)
|
||||
}
|
131
vendor/github.com/charmbracelet/bubbletea/key_sequences.go
generated
vendored
Normal file
131
vendor/github.com/charmbracelet/bubbletea/key_sequences.go
generated
vendored
Normal file
@ -0,0 +1,131 @@
|
||||
package tea
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sort"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// extSequences is used by the map-based algorithm below. It contains
|
||||
// the sequences plus their alternatives with an escape character
|
||||
// prefixed, plus the control chars, plus the space.
|
||||
// It does not contain the NUL character, which is handled specially
|
||||
// by detectOneMsg.
|
||||
var extSequences = func() map[string]Key {
|
||||
s := map[string]Key{}
|
||||
for seq, key := range sequences {
|
||||
key := key
|
||||
s[seq] = key
|
||||
if !key.Alt {
|
||||
key.Alt = true
|
||||
s["\x1b"+seq] = key
|
||||
}
|
||||
}
|
||||
for i := keyNUL + 1; i <= keyDEL; i++ {
|
||||
if i == keyESC {
|
||||
continue
|
||||
}
|
||||
s[string([]byte{byte(i)})] = Key{Type: i}
|
||||
s[string([]byte{'\x1b', byte(i)})] = Key{Type: i, Alt: true}
|
||||
if i == keyUS {
|
||||
i = keyDEL - 1
|
||||
}
|
||||
}
|
||||
s[" "] = Key{Type: KeySpace, Runes: spaceRunes}
|
||||
s["\x1b "] = Key{Type: KeySpace, Alt: true, Runes: spaceRunes}
|
||||
s["\x1b\x1b"] = Key{Type: KeyEscape, Alt: true}
|
||||
return s
|
||||
}()
|
||||
|
||||
// seqLengths is the sizes of valid sequences, starting with the
|
||||
// largest size.
|
||||
var seqLengths = func() []int {
|
||||
sizes := map[int]struct{}{}
|
||||
for seq := range extSequences {
|
||||
sizes[len(seq)] = struct{}{}
|
||||
}
|
||||
lsizes := make([]int, 0, len(sizes))
|
||||
for sz := range sizes {
|
||||
lsizes = append(lsizes, sz)
|
||||
}
|
||||
sort.Slice(lsizes, func(i, j int) bool { return lsizes[i] > lsizes[j] })
|
||||
return lsizes
|
||||
}()
|
||||
|
||||
// detectSequence uses a longest prefix match over the input
|
||||
// sequence and a hash map.
|
||||
func detectSequence(input []byte) (hasSeq bool, width int, msg Msg) {
|
||||
seqs := extSequences
|
||||
for _, sz := range seqLengths {
|
||||
if sz > len(input) {
|
||||
continue
|
||||
}
|
||||
prefix := input[:sz]
|
||||
key, ok := seqs[string(prefix)]
|
||||
if ok {
|
||||
return true, sz, KeyMsg(key)
|
||||
}
|
||||
}
|
||||
// Is this an unknown CSI sequence?
|
||||
if loc := unknownCSIRe.FindIndex(input); loc != nil {
|
||||
return true, loc[1], unknownCSISequenceMsg(input[:loc[1]])
|
||||
}
|
||||
|
||||
return false, 0, nil
|
||||
}
|
||||
|
||||
// detectBracketedPaste detects an input pasted while bracketed
|
||||
// paste mode was enabled.
|
||||
//
|
||||
// Note: this function is a no-op if bracketed paste was not enabled
|
||||
// on the terminal, since in that case we'd never see this
|
||||
// particular escape sequence.
|
||||
func detectBracketedPaste(input []byte) (hasBp bool, width int, msg Msg) {
|
||||
// Detect the start sequence.
|
||||
const bpStart = "\x1b[200~"
|
||||
if len(input) < len(bpStart) || string(input[:len(bpStart)]) != bpStart {
|
||||
return false, 0, nil
|
||||
}
|
||||
|
||||
// Skip over the start sequence.
|
||||
input = input[len(bpStart):]
|
||||
|
||||
// If we saw the start sequence, then we must have an end sequence
|
||||
// as well. Find it.
|
||||
const bpEnd = "\x1b[201~"
|
||||
idx := bytes.Index(input, []byte(bpEnd))
|
||||
inputLen := len(bpStart) + idx + len(bpEnd)
|
||||
if idx == -1 {
|
||||
// We have encountered the end of the input buffer without seeing
|
||||
// the marker for the end of the bracketed paste.
|
||||
// Tell the outer loop we have done a short read and we want more.
|
||||
return true, 0, nil
|
||||
}
|
||||
|
||||
// The paste is everything in-between.
|
||||
paste := input[:idx]
|
||||
|
||||
// All there is in-between is runes, not to be interpreted further.
|
||||
k := Key{Type: KeyRunes, Paste: true}
|
||||
for len(paste) > 0 {
|
||||
r, w := utf8.DecodeRune(paste)
|
||||
if r != utf8.RuneError {
|
||||
k.Runes = append(k.Runes, r)
|
||||
}
|
||||
paste = paste[w:]
|
||||
}
|
||||
|
||||
return true, inputLen, KeyMsg(k)
|
||||
}
|
||||
|
||||
// detectReportFocus detects a focus report sequence.
|
||||
// nolint: gomnd
|
||||
func detectReportFocus(input []byte) (hasRF bool, width int, msg Msg) {
|
||||
switch {
|
||||
case bytes.Equal(input, []byte("\x1b[I")):
|
||||
return true, 3, FocusMsg{}
|
||||
case bytes.Equal(input, []byte("\x1b[O")):
|
||||
return true, 3, BlurMsg{}
|
||||
}
|
||||
return false, 0, nil
|
||||
}
|
360
vendor/github.com/charmbracelet/bubbletea/key_windows.go
generated
vendored
Normal file
360
vendor/github.com/charmbracelet/bubbletea/key_windows.go
generated
vendored
Normal file
@ -0,0 +1,360 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package tea
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/erikgeiser/coninput"
|
||||
localereader "github.com/mattn/go-localereader"
|
||||
"github.com/muesli/cancelreader"
|
||||
)
|
||||
|
||||
func readInputs(ctx context.Context, msgs chan<- Msg, input io.Reader) error {
|
||||
if coninReader, ok := input.(*conInputReader); ok {
|
||||
return readConInputs(ctx, msgs, coninReader)
|
||||
}
|
||||
|
||||
return readAnsiInputs(ctx, msgs, localereader.NewReader(input))
|
||||
}
|
||||
|
||||
func readConInputs(ctx context.Context, msgsch chan<- Msg, con *conInputReader) error {
|
||||
var ps coninput.ButtonState // keep track of previous mouse state
|
||||
var ws coninput.WindowBufferSizeEventRecord // keep track of the last window size event
|
||||
for {
|
||||
events, err := coninput.ReadNConsoleInputs(con.conin, 16)
|
||||
if err != nil {
|
||||
if con.isCanceled() {
|
||||
return cancelreader.ErrCanceled
|
||||
}
|
||||
return fmt.Errorf("read coninput events: %w", err)
|
||||
}
|
||||
|
||||
for _, event := range events {
|
||||
var msgs []Msg
|
||||
switch e := event.Unwrap().(type) {
|
||||
case coninput.KeyEventRecord:
|
||||
if !e.KeyDown || e.VirtualKeyCode == coninput.VK_SHIFT {
|
||||
continue
|
||||
}
|
||||
|
||||
for i := 0; i < int(e.RepeatCount); i++ {
|
||||
eventKeyType := keyType(e)
|
||||
var runes []rune
|
||||
|
||||
// Add the character only if the key type is an actual character and not a control sequence.
|
||||
// This mimics the behavior in readAnsiInputs where the character is also removed.
|
||||
// We don't need to handle KeySpace here. See the comment in keyType().
|
||||
if eventKeyType == KeyRunes {
|
||||
runes = []rune{e.Char}
|
||||
}
|
||||
|
||||
msgs = append(msgs, KeyMsg{
|
||||
Type: eventKeyType,
|
||||
Runes: runes,
|
||||
Alt: e.ControlKeyState.Contains(coninput.LEFT_ALT_PRESSED | coninput.RIGHT_ALT_PRESSED),
|
||||
})
|
||||
}
|
||||
case coninput.WindowBufferSizeEventRecord:
|
||||
if e != ws {
|
||||
ws = e
|
||||
msgs = append(msgs, WindowSizeMsg{
|
||||
Width: int(e.Size.X),
|
||||
Height: int(e.Size.Y),
|
||||
})
|
||||
}
|
||||
case coninput.MouseEventRecord:
|
||||
event := mouseEvent(ps, e)
|
||||
if event.Type != MouseUnknown {
|
||||
msgs = append(msgs, event)
|
||||
}
|
||||
ps = e.ButtonState
|
||||
case coninput.FocusEventRecord, coninput.MenuEventRecord:
|
||||
// ignore
|
||||
default: // unknown event
|
||||
continue
|
||||
}
|
||||
|
||||
// Send all messages to the channel
|
||||
for _, msg := range msgs {
|
||||
select {
|
||||
case msgsch <- msg:
|
||||
case <-ctx.Done():
|
||||
err := ctx.Err()
|
||||
if err != nil {
|
||||
return fmt.Errorf("coninput context error: %w", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mouseEventButton(p, s coninput.ButtonState) (button MouseButton, action MouseAction) {
|
||||
btn := p ^ s
|
||||
action = MouseActionPress
|
||||
if btn&s == 0 {
|
||||
action = MouseActionRelease
|
||||
}
|
||||
|
||||
if btn == 0 {
|
||||
switch {
|
||||
case s&coninput.FROM_LEFT_1ST_BUTTON_PRESSED > 0:
|
||||
button = MouseButtonLeft
|
||||
case s&coninput.FROM_LEFT_2ND_BUTTON_PRESSED > 0:
|
||||
button = MouseButtonMiddle
|
||||
case s&coninput.RIGHTMOST_BUTTON_PRESSED > 0:
|
||||
button = MouseButtonRight
|
||||
case s&coninput.FROM_LEFT_3RD_BUTTON_PRESSED > 0:
|
||||
button = MouseButtonBackward
|
||||
case s&coninput.FROM_LEFT_4TH_BUTTON_PRESSED > 0:
|
||||
button = MouseButtonForward
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case btn == coninput.FROM_LEFT_1ST_BUTTON_PRESSED: // left button
|
||||
button = MouseButtonLeft
|
||||
case btn == coninput.RIGHTMOST_BUTTON_PRESSED: // right button
|
||||
button = MouseButtonRight
|
||||
case btn == coninput.FROM_LEFT_2ND_BUTTON_PRESSED: // middle button
|
||||
button = MouseButtonMiddle
|
||||
case btn == coninput.FROM_LEFT_3RD_BUTTON_PRESSED: // unknown (possibly mouse backward)
|
||||
button = MouseButtonBackward
|
||||
case btn == coninput.FROM_LEFT_4TH_BUTTON_PRESSED: // unknown (possibly mouse forward)
|
||||
button = MouseButtonForward
|
||||
}
|
||||
|
||||
return button, action
|
||||
}
|
||||
|
||||
func mouseEvent(p coninput.ButtonState, e coninput.MouseEventRecord) MouseMsg {
|
||||
ev := MouseMsg{
|
||||
X: int(e.MousePositon.X),
|
||||
Y: int(e.MousePositon.Y),
|
||||
Alt: e.ControlKeyState.Contains(coninput.LEFT_ALT_PRESSED | coninput.RIGHT_ALT_PRESSED),
|
||||
Ctrl: e.ControlKeyState.Contains(coninput.LEFT_CTRL_PRESSED | coninput.RIGHT_CTRL_PRESSED),
|
||||
Shift: e.ControlKeyState.Contains(coninput.SHIFT_PRESSED),
|
||||
}
|
||||
switch e.EventFlags {
|
||||
case coninput.CLICK, coninput.DOUBLE_CLICK:
|
||||
ev.Button, ev.Action = mouseEventButton(p, e.ButtonState)
|
||||
if ev.Action == MouseActionRelease {
|
||||
ev.Type = MouseRelease
|
||||
}
|
||||
switch ev.Button {
|
||||
case MouseButtonLeft:
|
||||
ev.Type = MouseLeft
|
||||
case MouseButtonMiddle:
|
||||
ev.Type = MouseMiddle
|
||||
case MouseButtonRight:
|
||||
ev.Type = MouseRight
|
||||
case MouseButtonBackward:
|
||||
ev.Type = MouseBackward
|
||||
case MouseButtonForward:
|
||||
ev.Type = MouseForward
|
||||
}
|
||||
case coninput.MOUSE_WHEELED:
|
||||
if e.WheelDirection > 0 {
|
||||
ev.Button = MouseButtonWheelUp
|
||||
ev.Type = MouseWheelUp
|
||||
} else {
|
||||
ev.Button = MouseButtonWheelDown
|
||||
ev.Type = MouseWheelDown
|
||||
}
|
||||
case coninput.MOUSE_HWHEELED:
|
||||
if e.WheelDirection > 0 {
|
||||
ev.Button = MouseButtonWheelRight
|
||||
ev.Type = MouseWheelRight
|
||||
} else {
|
||||
ev.Button = MouseButtonWheelLeft
|
||||
ev.Type = MouseWheelLeft
|
||||
}
|
||||
case coninput.MOUSE_MOVED:
|
||||
ev.Button, _ = mouseEventButton(p, e.ButtonState)
|
||||
ev.Action = MouseActionMotion
|
||||
ev.Type = MouseMotion
|
||||
}
|
||||
|
||||
return ev
|
||||
}
|
||||
|
||||
func keyType(e coninput.KeyEventRecord) KeyType {
|
||||
code := e.VirtualKeyCode
|
||||
|
||||
shiftPressed := e.ControlKeyState.Contains(coninput.SHIFT_PRESSED)
|
||||
ctrlPressed := e.ControlKeyState.Contains(coninput.LEFT_CTRL_PRESSED | coninput.RIGHT_CTRL_PRESSED)
|
||||
|
||||
switch code {
|
||||
case coninput.VK_RETURN:
|
||||
return KeyEnter
|
||||
case coninput.VK_BACK:
|
||||
return KeyBackspace
|
||||
case coninput.VK_TAB:
|
||||
if shiftPressed {
|
||||
return KeyShiftTab
|
||||
}
|
||||
return KeyTab
|
||||
case coninput.VK_SPACE:
|
||||
return KeyRunes // this could be KeySpace but on unix space also produces KeyRunes
|
||||
case coninput.VK_ESCAPE:
|
||||
return KeyEscape
|
||||
case coninput.VK_UP:
|
||||
switch {
|
||||
case shiftPressed && ctrlPressed:
|
||||
return KeyCtrlShiftUp
|
||||
case shiftPressed:
|
||||
return KeyShiftUp
|
||||
case ctrlPressed:
|
||||
return KeyCtrlUp
|
||||
default:
|
||||
return KeyUp
|
||||
}
|
||||
case coninput.VK_DOWN:
|
||||
switch {
|
||||
case shiftPressed && ctrlPressed:
|
||||
return KeyCtrlShiftDown
|
||||
case shiftPressed:
|
||||
return KeyShiftDown
|
||||
case ctrlPressed:
|
||||
return KeyCtrlDown
|
||||
default:
|
||||
return KeyDown
|
||||
}
|
||||
case coninput.VK_RIGHT:
|
||||
switch {
|
||||
case shiftPressed && ctrlPressed:
|
||||
return KeyCtrlShiftRight
|
||||
case shiftPressed:
|
||||
return KeyShiftRight
|
||||
case ctrlPressed:
|
||||
return KeyCtrlRight
|
||||
default:
|
||||
return KeyRight
|
||||
}
|
||||
case coninput.VK_LEFT:
|
||||
switch {
|
||||
case shiftPressed && ctrlPressed:
|
||||
return KeyCtrlShiftLeft
|
||||
case shiftPressed:
|
||||
return KeyShiftLeft
|
||||
case ctrlPressed:
|
||||
return KeyCtrlLeft
|
||||
default:
|
||||
return KeyLeft
|
||||
}
|
||||
case coninput.VK_HOME:
|
||||
switch {
|
||||
case shiftPressed && ctrlPressed:
|
||||
return KeyCtrlShiftHome
|
||||
case shiftPressed:
|
||||
return KeyShiftHome
|
||||
case ctrlPressed:
|
||||
return KeyCtrlHome
|
||||
default:
|
||||
return KeyHome
|
||||
}
|
||||
case coninput.VK_END:
|
||||
switch {
|
||||
case shiftPressed && ctrlPressed:
|
||||
return KeyCtrlShiftEnd
|
||||
case shiftPressed:
|
||||
return KeyShiftEnd
|
||||
case ctrlPressed:
|
||||
return KeyCtrlEnd
|
||||
default:
|
||||
return KeyEnd
|
||||
}
|
||||
case coninput.VK_PRIOR:
|
||||
return KeyPgUp
|
||||
case coninput.VK_NEXT:
|
||||
return KeyPgDown
|
||||
case coninput.VK_DELETE:
|
||||
return KeyDelete
|
||||
default:
|
||||
switch {
|
||||
case e.ControlKeyState.Contains(coninput.LEFT_CTRL_PRESSED) && e.ControlKeyState.Contains(coninput.RIGHT_ALT_PRESSED):
|
||||
// AltGr is pressed, then it's a rune.
|
||||
fallthrough
|
||||
case !e.ControlKeyState.Contains(coninput.LEFT_CTRL_PRESSED) && !e.ControlKeyState.Contains(coninput.RIGHT_CTRL_PRESSED):
|
||||
return KeyRunes
|
||||
}
|
||||
|
||||
switch e.Char {
|
||||
case '@':
|
||||
return KeyCtrlAt
|
||||
case '\x01':
|
||||
return KeyCtrlA
|
||||
case '\x02':
|
||||
return KeyCtrlB
|
||||
case '\x03':
|
||||
return KeyCtrlC
|
||||
case '\x04':
|
||||
return KeyCtrlD
|
||||
case '\x05':
|
||||
return KeyCtrlE
|
||||
case '\x06':
|
||||
return KeyCtrlF
|
||||
case '\a':
|
||||
return KeyCtrlG
|
||||
case '\b':
|
||||
return KeyCtrlH
|
||||
case '\t':
|
||||
return KeyCtrlI
|
||||
case '\n':
|
||||
return KeyCtrlJ
|
||||
case '\v':
|
||||
return KeyCtrlK
|
||||
case '\f':
|
||||
return KeyCtrlL
|
||||
case '\r':
|
||||
return KeyCtrlM
|
||||
case '\x0e':
|
||||
return KeyCtrlN
|
||||
case '\x0f':
|
||||
return KeyCtrlO
|
||||
case '\x10':
|
||||
return KeyCtrlP
|
||||
case '\x11':
|
||||
return KeyCtrlQ
|
||||
case '\x12':
|
||||
return KeyCtrlR
|
||||
case '\x13':
|
||||
return KeyCtrlS
|
||||
case '\x14':
|
||||
return KeyCtrlT
|
||||
case '\x15':
|
||||
return KeyCtrlU
|
||||
case '\x16':
|
||||
return KeyCtrlV
|
||||
case '\x17':
|
||||
return KeyCtrlW
|
||||
case '\x18':
|
||||
return KeyCtrlX
|
||||
case '\x19':
|
||||
return KeyCtrlY
|
||||
case '\x1a':
|
||||
return KeyCtrlZ
|
||||
case '\x1b':
|
||||
return KeyCtrlOpenBracket // KeyEscape
|
||||
case '\x1c':
|
||||
return KeyCtrlBackslash
|
||||
case '\x1f':
|
||||
return KeyCtrlUnderscore
|
||||
}
|
||||
|
||||
switch code {
|
||||
case coninput.VK_OEM_4:
|
||||
return KeyCtrlOpenBracket
|
||||
case coninput.VK_OEM_6:
|
||||
return KeyCtrlCloseBracket
|
||||
}
|
||||
|
||||
return KeyRunes
|
||||
}
|
||||
}
|
53
vendor/github.com/charmbracelet/bubbletea/logging.go
generated
vendored
Normal file
53
vendor/github.com/charmbracelet/bubbletea/logging.go
generated
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
package tea
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// LogToFile sets up default logging to log to a file. This is helpful as we
|
||||
// can't print to the terminal since our TUI is occupying it. If the file
|
||||
// doesn't exist it will be created.
|
||||
//
|
||||
// Don't forget to close the file when you're done with it.
|
||||
//
|
||||
// f, err := LogToFile("debug.log", "debug")
|
||||
// if err != nil {
|
||||
// fmt.Println("fatal:", err)
|
||||
// os.Exit(1)
|
||||
// }
|
||||
// defer f.Close()
|
||||
func LogToFile(path string, prefix string) (*os.File, error) {
|
||||
return LogToFileWith(path, prefix, log.Default())
|
||||
}
|
||||
|
||||
// LogOptionsSetter is an interface implemented by stdlib's log and charm's log
|
||||
// libraries.
|
||||
type LogOptionsSetter interface {
|
||||
SetOutput(io.Writer)
|
||||
SetPrefix(string)
|
||||
}
|
||||
|
||||
// LogToFileWith does allows to call LogToFile with a custom LogOptionsSetter.
|
||||
func LogToFileWith(path string, prefix string, log LogOptionsSetter) (*os.File, error) {
|
||||
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o600) //nolint:gomnd
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error opening file for logging: %w", err)
|
||||
}
|
||||
log.SetOutput(f)
|
||||
|
||||
// Add a space after the prefix if a prefix is being specified and it
|
||||
// doesn't already have a trailing space.
|
||||
if len(prefix) > 0 {
|
||||
finalChar := prefix[len(prefix)-1]
|
||||
if !unicode.IsSpace(rune(finalChar)) {
|
||||
prefix += " "
|
||||
}
|
||||
}
|
||||
log.SetPrefix(prefix)
|
||||
|
||||
return f, nil
|
||||
}
|
308
vendor/github.com/charmbracelet/bubbletea/mouse.go
generated
vendored
Normal file
308
vendor/github.com/charmbracelet/bubbletea/mouse.go
generated
vendored
Normal file
@ -0,0 +1,308 @@
|
||||
package tea
|
||||
|
||||
import "strconv"
|
||||
|
||||
// MouseMsg contains information about a mouse event and are sent to a programs
|
||||
// update function when mouse activity occurs. Note that the mouse must first
|
||||
// be enabled in order for the mouse events to be received.
|
||||
type MouseMsg MouseEvent
|
||||
|
||||
// String returns a string representation of a mouse event.
|
||||
func (m MouseMsg) String() string {
|
||||
return MouseEvent(m).String()
|
||||
}
|
||||
|
||||
// MouseEvent represents a mouse event, which could be a click, a scroll wheel
|
||||
// movement, a cursor movement, or a combination.
|
||||
type MouseEvent struct {
|
||||
X int
|
||||
Y int
|
||||
Shift bool
|
||||
Alt bool
|
||||
Ctrl bool
|
||||
Action MouseAction
|
||||
Button MouseButton
|
||||
|
||||
// Deprecated: Use MouseAction & MouseButton instead.
|
||||
Type MouseEventType
|
||||
}
|
||||
|
||||
// IsWheel returns true if the mouse event is a wheel event.
|
||||
func (m MouseEvent) IsWheel() bool {
|
||||
return m.Button == MouseButtonWheelUp || m.Button == MouseButtonWheelDown ||
|
||||
m.Button == MouseButtonWheelLeft || m.Button == MouseButtonWheelRight
|
||||
}
|
||||
|
||||
// String returns a string representation of a mouse event.
|
||||
func (m MouseEvent) String() (s string) {
|
||||
if m.Ctrl {
|
||||
s += "ctrl+"
|
||||
}
|
||||
if m.Alt {
|
||||
s += "alt+"
|
||||
}
|
||||
if m.Shift {
|
||||
s += "shift+"
|
||||
}
|
||||
|
||||
if m.Button == MouseButtonNone { //nolint:nestif
|
||||
if m.Action == MouseActionMotion || m.Action == MouseActionRelease {
|
||||
s += mouseActions[m.Action]
|
||||
} else {
|
||||
s += "unknown"
|
||||
}
|
||||
} else if m.IsWheel() {
|
||||
s += mouseButtons[m.Button]
|
||||
} else {
|
||||
btn := mouseButtons[m.Button]
|
||||
if btn != "" {
|
||||
s += btn
|
||||
}
|
||||
act := mouseActions[m.Action]
|
||||
if act != "" {
|
||||
s += " " + act
|
||||
}
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// MouseAction represents the action that occurred during a mouse event.
|
||||
type MouseAction int
|
||||
|
||||
// Mouse event actions.
|
||||
const (
|
||||
MouseActionPress MouseAction = iota
|
||||
MouseActionRelease
|
||||
MouseActionMotion
|
||||
)
|
||||
|
||||
var mouseActions = map[MouseAction]string{
|
||||
MouseActionPress: "press",
|
||||
MouseActionRelease: "release",
|
||||
MouseActionMotion: "motion",
|
||||
}
|
||||
|
||||
// MouseButton represents the button that was pressed during a mouse event.
|
||||
type MouseButton int
|
||||
|
||||
// Mouse event buttons
|
||||
//
|
||||
// This is based on X11 mouse button codes.
|
||||
//
|
||||
// 1 = left button
|
||||
// 2 = middle button (pressing the scroll wheel)
|
||||
// 3 = right button
|
||||
// 4 = turn scroll wheel up
|
||||
// 5 = turn scroll wheel down
|
||||
// 6 = push scroll wheel left
|
||||
// 7 = push scroll wheel right
|
||||
// 8 = 4th button (aka browser backward button)
|
||||
// 9 = 5th button (aka browser forward button)
|
||||
// 10
|
||||
// 11
|
||||
//
|
||||
// Other buttons are not supported.
|
||||
const (
|
||||
MouseButtonNone MouseButton = iota
|
||||
MouseButtonLeft
|
||||
MouseButtonMiddle
|
||||
MouseButtonRight
|
||||
MouseButtonWheelUp
|
||||
MouseButtonWheelDown
|
||||
MouseButtonWheelLeft
|
||||
MouseButtonWheelRight
|
||||
MouseButtonBackward
|
||||
MouseButtonForward
|
||||
MouseButton10
|
||||
MouseButton11
|
||||
)
|
||||
|
||||
var mouseButtons = map[MouseButton]string{
|
||||
MouseButtonNone: "none",
|
||||
MouseButtonLeft: "left",
|
||||
MouseButtonMiddle: "middle",
|
||||
MouseButtonRight: "right",
|
||||
MouseButtonWheelUp: "wheel up",
|
||||
MouseButtonWheelDown: "wheel down",
|
||||
MouseButtonWheelLeft: "wheel left",
|
||||
MouseButtonWheelRight: "wheel right",
|
||||
MouseButtonBackward: "backward",
|
||||
MouseButtonForward: "forward",
|
||||
MouseButton10: "button 10",
|
||||
MouseButton11: "button 11",
|
||||
}
|
||||
|
||||
// MouseEventType indicates the type of mouse event occurring.
|
||||
//
|
||||
// Deprecated: Use MouseAction & MouseButton instead.
|
||||
type MouseEventType int
|
||||
|
||||
// Mouse event types.
|
||||
//
|
||||
// Deprecated: Use MouseAction & MouseButton instead.
|
||||
const (
|
||||
MouseUnknown MouseEventType = iota
|
||||
MouseLeft
|
||||
MouseRight
|
||||
MouseMiddle
|
||||
MouseRelease // mouse button release (X10 only)
|
||||
MouseWheelUp
|
||||
MouseWheelDown
|
||||
MouseWheelLeft
|
||||
MouseWheelRight
|
||||
MouseBackward
|
||||
MouseForward
|
||||
MouseMotion
|
||||
)
|
||||
|
||||
// Parse SGR-encoded mouse events; SGR extended mouse events. SGR mouse events
|
||||
// look like:
|
||||
//
|
||||
// ESC [ < Cb ; Cx ; Cy (M or m)
|
||||
//
|
||||
// where:
|
||||
//
|
||||
// Cb is the encoded button code
|
||||
// Cx is the x-coordinate of the mouse
|
||||
// Cy is the y-coordinate of the mouse
|
||||
// M is for button press, m is for button release
|
||||
//
|
||||
// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates
|
||||
func parseSGRMouseEvent(buf []byte) MouseEvent {
|
||||
str := string(buf[3:])
|
||||
matches := mouseSGRRegex.FindStringSubmatch(str)
|
||||
if len(matches) != 5 { //nolint:gomnd
|
||||
// Unreachable, we already checked the regex in `detectOneMsg`.
|
||||
panic("invalid mouse event")
|
||||
}
|
||||
|
||||
b, _ := strconv.Atoi(matches[1])
|
||||
px := matches[2]
|
||||
py := matches[3]
|
||||
release := matches[4] == "m"
|
||||
m := parseMouseButton(b, true)
|
||||
|
||||
// Wheel buttons don't have release events
|
||||
// Motion can be reported as a release event in some terminals (Windows Terminal)
|
||||
if m.Action != MouseActionMotion && !m.IsWheel() && release {
|
||||
m.Action = MouseActionRelease
|
||||
m.Type = MouseRelease
|
||||
}
|
||||
|
||||
x, _ := strconv.Atoi(px)
|
||||
y, _ := strconv.Atoi(py)
|
||||
|
||||
// (1,1) is the upper left. We subtract 1 to normalize it to (0,0).
|
||||
m.X = x - 1
|
||||
m.Y = y - 1
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
const x10MouseByteOffset = 32
|
||||
|
||||
// Parse X10-encoded mouse events; the simplest kind. The last release of X10
|
||||
// was December 1986, by the way. The original X10 mouse protocol limits the Cx
|
||||
// and Cy coordinates to 223 (=255-032).
|
||||
//
|
||||
// X10 mouse events look like:
|
||||
//
|
||||
// ESC [M Cb Cx Cy
|
||||
//
|
||||
// See: http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking
|
||||
func parseX10MouseEvent(buf []byte) MouseEvent {
|
||||
v := buf[3:6]
|
||||
m := parseMouseButton(int(v[0]), false)
|
||||
|
||||
// (1,1) is the upper left. We subtract 1 to normalize it to (0,0).
|
||||
m.X = int(v[1]) - x10MouseByteOffset - 1
|
||||
m.Y = int(v[2]) - x10MouseByteOffset - 1
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates
|
||||
func parseMouseButton(b int, isSGR bool) MouseEvent {
|
||||
var m MouseEvent
|
||||
e := b
|
||||
if !isSGR {
|
||||
e -= x10MouseByteOffset
|
||||
}
|
||||
|
||||
const (
|
||||
bitShift = 0b0000_0100
|
||||
bitAlt = 0b0000_1000
|
||||
bitCtrl = 0b0001_0000
|
||||
bitMotion = 0b0010_0000
|
||||
bitWheel = 0b0100_0000
|
||||
bitAdd = 0b1000_0000 // additional buttons 8-11
|
||||
|
||||
bitsMask = 0b0000_0011
|
||||
)
|
||||
|
||||
if e&bitAdd != 0 {
|
||||
m.Button = MouseButtonBackward + MouseButton(e&bitsMask)
|
||||
} else if e&bitWheel != 0 {
|
||||
m.Button = MouseButtonWheelUp + MouseButton(e&bitsMask)
|
||||
} else {
|
||||
m.Button = MouseButtonLeft + MouseButton(e&bitsMask)
|
||||
// X10 reports a button release as 0b0000_0011 (3)
|
||||
if e&bitsMask == bitsMask {
|
||||
m.Action = MouseActionRelease
|
||||
m.Button = MouseButtonNone
|
||||
}
|
||||
}
|
||||
|
||||
// Motion bit doesn't get reported for wheel events.
|
||||
if e&bitMotion != 0 && !m.IsWheel() {
|
||||
m.Action = MouseActionMotion
|
||||
}
|
||||
|
||||
// Modifiers
|
||||
m.Alt = e&bitAlt != 0
|
||||
m.Ctrl = e&bitCtrl != 0
|
||||
m.Shift = e&bitShift != 0
|
||||
|
||||
// backward compatibility
|
||||
switch {
|
||||
case m.Button == MouseButtonLeft && m.Action == MouseActionPress:
|
||||
m.Type = MouseLeft
|
||||
case m.Button == MouseButtonMiddle && m.Action == MouseActionPress:
|
||||
m.Type = MouseMiddle
|
||||
case m.Button == MouseButtonRight && m.Action == MouseActionPress:
|
||||
m.Type = MouseRight
|
||||
case m.Button == MouseButtonNone && m.Action == MouseActionRelease:
|
||||
m.Type = MouseRelease
|
||||
case m.Button == MouseButtonWheelUp && m.Action == MouseActionPress:
|
||||
m.Type = MouseWheelUp
|
||||
case m.Button == MouseButtonWheelDown && m.Action == MouseActionPress:
|
||||
m.Type = MouseWheelDown
|
||||
case m.Button == MouseButtonWheelLeft && m.Action == MouseActionPress:
|
||||
m.Type = MouseWheelLeft
|
||||
case m.Button == MouseButtonWheelRight && m.Action == MouseActionPress:
|
||||
m.Type = MouseWheelRight
|
||||
case m.Button == MouseButtonBackward && m.Action == MouseActionPress:
|
||||
m.Type = MouseBackward
|
||||
case m.Button == MouseButtonForward && m.Action == MouseActionPress:
|
||||
m.Type = MouseForward
|
||||
case m.Action == MouseActionMotion:
|
||||
m.Type = MouseMotion
|
||||
switch m.Button { //nolint:exhaustive
|
||||
case MouseButtonLeft:
|
||||
m.Type = MouseLeft
|
||||
case MouseButtonMiddle:
|
||||
m.Type = MouseMiddle
|
||||
case MouseButtonRight:
|
||||
m.Type = MouseRight
|
||||
case MouseButtonBackward:
|
||||
m.Type = MouseBackward
|
||||
case MouseButtonForward:
|
||||
m.Type = MouseForward
|
||||
}
|
||||
default:
|
||||
m.Type = MouseUnknown
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
28
vendor/github.com/charmbracelet/bubbletea/nil_renderer.go
generated
vendored
Normal file
28
vendor/github.com/charmbracelet/bubbletea/nil_renderer.go
generated
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
package tea
|
||||
|
||||
type nilRenderer struct{}
|
||||
|
||||
func (n nilRenderer) start() {}
|
||||
func (n nilRenderer) stop() {}
|
||||
func (n nilRenderer) kill() {}
|
||||
func (n nilRenderer) write(_ string) {}
|
||||
func (n nilRenderer) repaint() {}
|
||||
func (n nilRenderer) clearScreen() {}
|
||||
func (n nilRenderer) altScreen() bool { return false }
|
||||
func (n nilRenderer) enterAltScreen() {}
|
||||
func (n nilRenderer) exitAltScreen() {}
|
||||
func (n nilRenderer) showCursor() {}
|
||||
func (n nilRenderer) hideCursor() {}
|
||||
func (n nilRenderer) enableMouseCellMotion() {}
|
||||
func (n nilRenderer) disableMouseCellMotion() {}
|
||||
func (n nilRenderer) enableMouseAllMotion() {}
|
||||
func (n nilRenderer) disableMouseAllMotion() {}
|
||||
func (n nilRenderer) enableBracketedPaste() {}
|
||||
func (n nilRenderer) disableBracketedPaste() {}
|
||||
func (n nilRenderer) enableMouseSGRMode() {}
|
||||
func (n nilRenderer) disableMouseSGRMode() {}
|
||||
func (n nilRenderer) bracketedPasteActive() bool { return false }
|
||||
func (n nilRenderer) setWindowTitle(_ string) {}
|
||||
func (n nilRenderer) reportFocus() bool { return false }
|
||||
func (n nilRenderer) enableReportFocus() {}
|
||||
func (n nilRenderer) disableReportFocus() {}
|
252
vendor/github.com/charmbracelet/bubbletea/options.go
generated
vendored
Normal file
252
vendor/github.com/charmbracelet/bubbletea/options.go
generated
vendored
Normal file
@ -0,0 +1,252 @@
|
||||
package tea
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// ProgramOption is used to set options when initializing a Program. Program can
|
||||
// accept a variable number of options.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// p := NewProgram(model, WithInput(someInput), WithOutput(someOutput))
|
||||
type ProgramOption func(*Program)
|
||||
|
||||
// WithContext lets you specify a context in which to run the Program. This is
|
||||
// useful if you want to cancel the execution from outside. When a Program gets
|
||||
// cancelled it will exit with an error ErrProgramKilled.
|
||||
func WithContext(ctx context.Context) ProgramOption {
|
||||
return func(p *Program) {
|
||||
p.ctx = ctx
|
||||
}
|
||||
}
|
||||
|
||||
// WithOutput sets the output which, by default, is stdout. In most cases you
|
||||
// won't need to use this.
|
||||
func WithOutput(output io.Writer) ProgramOption {
|
||||
return func(p *Program) {
|
||||
p.output = output
|
||||
}
|
||||
}
|
||||
|
||||
// WithInput sets the input which, by default, is stdin. In most cases you
|
||||
// won't need to use this. To disable input entirely pass nil.
|
||||
//
|
||||
// p := NewProgram(model, WithInput(nil))
|
||||
func WithInput(input io.Reader) ProgramOption {
|
||||
return func(p *Program) {
|
||||
p.input = input
|
||||
p.inputType = customInput
|
||||
}
|
||||
}
|
||||
|
||||
// WithInputTTY opens a new TTY for input (or console input device on Windows).
|
||||
func WithInputTTY() ProgramOption {
|
||||
return func(p *Program) {
|
||||
p.inputType = ttyInput
|
||||
}
|
||||
}
|
||||
|
||||
// WithEnvironment sets the environment variables that the program will use.
|
||||
// This useful when the program is running in a remote session (e.g. SSH) and
|
||||
// you want to pass the environment variables from the remote session to the
|
||||
// program.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// var sess ssh.Session // ssh.Session is a type from the github.com/charmbracelet/ssh package
|
||||
// pty, _, _ := sess.Pty()
|
||||
// environ := append(sess.Environ(), "TERM="+pty.Term)
|
||||
// p := tea.NewProgram(model, tea.WithEnvironment(environ)
|
||||
func WithEnvironment(env []string) ProgramOption {
|
||||
return func(p *Program) {
|
||||
p.environ = env
|
||||
}
|
||||
}
|
||||
|
||||
// WithoutSignalHandler disables the signal handler that Bubble Tea sets up for
|
||||
// Programs. This is useful if you want to handle signals yourself.
|
||||
func WithoutSignalHandler() ProgramOption {
|
||||
return func(p *Program) {
|
||||
p.startupOptions |= withoutSignalHandler
|
||||
}
|
||||
}
|
||||
|
||||
// WithoutCatchPanics disables the panic catching that Bubble Tea does by
|
||||
// default. If panic catching is disabled the terminal will be in a fairly
|
||||
// unusable state after a panic because Bubble Tea will not perform its usual
|
||||
// cleanup on exit.
|
||||
func WithoutCatchPanics() ProgramOption {
|
||||
return func(p *Program) {
|
||||
p.startupOptions |= withoutCatchPanics
|
||||
}
|
||||
}
|
||||
|
||||
// WithoutSignals will ignore OS signals.
|
||||
// This is mainly useful for testing.
|
||||
func WithoutSignals() ProgramOption {
|
||||
return func(p *Program) {
|
||||
atomic.StoreUint32(&p.ignoreSignals, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// WithAltScreen starts the program with the alternate screen buffer enabled
|
||||
// (i.e. the program starts in full window mode). Note that the altscreen will
|
||||
// be automatically exited when the program quits.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// p := tea.NewProgram(Model{}, tea.WithAltScreen())
|
||||
// if _, err := p.Run(); err != nil {
|
||||
// fmt.Println("Error running program:", err)
|
||||
// os.Exit(1)
|
||||
// }
|
||||
//
|
||||
// To enter the altscreen once the program has already started running use the
|
||||
// EnterAltScreen command.
|
||||
func WithAltScreen() ProgramOption {
|
||||
return func(p *Program) {
|
||||
p.startupOptions |= withAltScreen
|
||||
}
|
||||
}
|
||||
|
||||
// WithoutBracketedPaste starts the program with bracketed paste disabled.
|
||||
func WithoutBracketedPaste() ProgramOption {
|
||||
return func(p *Program) {
|
||||
p.startupOptions |= withoutBracketedPaste
|
||||
}
|
||||
}
|
||||
|
||||
// WithMouseCellMotion starts the program with the mouse enabled in "cell
|
||||
// motion" mode.
|
||||
//
|
||||
// Cell motion mode enables mouse click, release, and wheel events. Mouse
|
||||
// movement events are also captured if a mouse button is pressed (i.e., drag
|
||||
// events). Cell motion mode is better supported than all motion mode.
|
||||
//
|
||||
// This will try to enable the mouse in extended mode (SGR), if that is not
|
||||
// supported by the terminal it will fall back to normal mode (X10).
|
||||
//
|
||||
// To enable mouse cell motion once the program has already started running use
|
||||
// the EnableMouseCellMotion command. To disable the mouse when the program is
|
||||
// running use the DisableMouse command.
|
||||
//
|
||||
// The mouse will be automatically disabled when the program exits.
|
||||
func WithMouseCellMotion() ProgramOption {
|
||||
return func(p *Program) {
|
||||
p.startupOptions |= withMouseCellMotion // set
|
||||
p.startupOptions &^= withMouseAllMotion // clear
|
||||
}
|
||||
}
|
||||
|
||||
// WithMouseAllMotion starts the program with the mouse enabled in "all motion"
|
||||
// mode.
|
||||
//
|
||||
// EnableMouseAllMotion is a special command that enables mouse click, release,
|
||||
// wheel, and motion events, which are delivered regardless of whether a mouse
|
||||
// button is pressed, effectively enabling support for hover interactions.
|
||||
//
|
||||
// This will try to enable the mouse in extended mode (SGR), if that is not
|
||||
// supported by the terminal it will fall back to normal mode (X10).
|
||||
//
|
||||
// Many modern terminals support this, but not all. If in doubt, use
|
||||
// EnableMouseCellMotion instead.
|
||||
//
|
||||
// To enable the mouse once the program has already started running use the
|
||||
// EnableMouseAllMotion command. To disable the mouse when the program is
|
||||
// running use the DisableMouse command.
|
||||
//
|
||||
// The mouse will be automatically disabled when the program exits.
|
||||
func WithMouseAllMotion() ProgramOption {
|
||||
return func(p *Program) {
|
||||
p.startupOptions |= withMouseAllMotion // set
|
||||
p.startupOptions &^= withMouseCellMotion // clear
|
||||
}
|
||||
}
|
||||
|
||||
// WithoutRenderer disables the renderer. When this is set output and log
|
||||
// statements will be plainly sent to stdout (or another output if one is set)
|
||||
// without any rendering and redrawing logic. In other words, printing and
|
||||
// logging will behave the same way it would in a non-TUI commandline tool.
|
||||
// This can be useful if you want to use the Bubble Tea framework for a non-TUI
|
||||
// application, or to provide an additional non-TUI mode to your Bubble Tea
|
||||
// programs. For example, your program could behave like a daemon if output is
|
||||
// not a TTY.
|
||||
func WithoutRenderer() ProgramOption {
|
||||
return func(p *Program) {
|
||||
p.renderer = &nilRenderer{}
|
||||
}
|
||||
}
|
||||
|
||||
// WithANSICompressor removes redundant ANSI sequences to produce potentially
|
||||
// smaller output, at the cost of some processing overhead.
|
||||
//
|
||||
// This feature is provisional, and may be changed or removed in a future version
|
||||
// of this package.
|
||||
//
|
||||
// Deprecated: this incurs a noticeable performance hit. A future release will
|
||||
// optimize ANSI automatically without the performance penalty.
|
||||
func WithANSICompressor() ProgramOption {
|
||||
return func(p *Program) {
|
||||
p.startupOptions |= withANSICompressor
|
||||
}
|
||||
}
|
||||
|
||||
// WithFilter supplies an event filter that will be invoked before Bubble Tea
|
||||
// processes a tea.Msg. The event filter can return any tea.Msg which will then
|
||||
// get handled by Bubble Tea instead of the original event. If the event filter
|
||||
// returns nil, the event will be ignored and Bubble Tea will not process it.
|
||||
//
|
||||
// As an example, this could be used to prevent a program from shutting down if
|
||||
// there are unsaved changes.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func filter(m tea.Model, msg tea.Msg) tea.Msg {
|
||||
// if _, ok := msg.(tea.QuitMsg); !ok {
|
||||
// return msg
|
||||
// }
|
||||
//
|
||||
// model := m.(myModel)
|
||||
// if model.hasChanges {
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// return msg
|
||||
// }
|
||||
//
|
||||
// p := tea.NewProgram(Model{}, tea.WithFilter(filter));
|
||||
//
|
||||
// if _,err := p.Run(); err != nil {
|
||||
// fmt.Println("Error running program:", err)
|
||||
// os.Exit(1)
|
||||
// }
|
||||
func WithFilter(filter func(Model, Msg) Msg) ProgramOption {
|
||||
return func(p *Program) {
|
||||
p.filter = filter
|
||||
}
|
||||
}
|
||||
|
||||
// WithFPS sets a custom maximum FPS at which the renderer should run. If
|
||||
// less than 1, the default value of 60 will be used. If over 120, the FPS
|
||||
// will be capped at 120.
|
||||
func WithFPS(fps int) ProgramOption {
|
||||
return func(p *Program) {
|
||||
p.fps = fps
|
||||
}
|
||||
}
|
||||
|
||||
// WithReportFocus enables reporting when the terminal gains and loses
|
||||
// focus. When this is enabled [FocusMsg] and [BlurMsg] messages will be sent
|
||||
// to your Update method.
|
||||
//
|
||||
// Note that while most terminals and multiplexers support focus reporting,
|
||||
// some do not. Also note that tmux needs to be configured to report focus
|
||||
// events.
|
||||
func WithReportFocus() ProgramOption {
|
||||
return func(p *Program) {
|
||||
p.startupOptions |= withReportFocus
|
||||
}
|
||||
}
|
85
vendor/github.com/charmbracelet/bubbletea/renderer.go
generated
vendored
Normal file
85
vendor/github.com/charmbracelet/bubbletea/renderer.go
generated
vendored
Normal file
@ -0,0 +1,85 @@
|
||||
package tea
|
||||
|
||||
// renderer is the interface for Bubble Tea renderers.
|
||||
type renderer interface {
|
||||
// Start the renderer.
|
||||
start()
|
||||
|
||||
// Stop the renderer, but render the final frame in the buffer, if any.
|
||||
stop()
|
||||
|
||||
// Stop the renderer without doing any final rendering.
|
||||
kill()
|
||||
|
||||
// Write a frame to the renderer. The renderer can write this data to
|
||||
// output at its discretion.
|
||||
write(string)
|
||||
|
||||
// Request a full re-render. Note that this will not trigger a render
|
||||
// immediately. Rather, this method causes the next render to be a full
|
||||
// repaint. Because of this, it's safe to call this method multiple times
|
||||
// in succession.
|
||||
repaint()
|
||||
|
||||
// Clears the terminal.
|
||||
clearScreen()
|
||||
|
||||
// Whether or not the alternate screen buffer is enabled.
|
||||
altScreen() bool
|
||||
// Enable the alternate screen buffer.
|
||||
enterAltScreen()
|
||||
// Disable the alternate screen buffer.
|
||||
exitAltScreen()
|
||||
|
||||
// Show the cursor.
|
||||
showCursor()
|
||||
// Hide the cursor.
|
||||
hideCursor()
|
||||
|
||||
// enableMouseCellMotion enables mouse click, release, wheel and motion
|
||||
// events if a mouse button is pressed (i.e., drag events).
|
||||
enableMouseCellMotion()
|
||||
|
||||
// disableMouseCellMotion disables Mouse Cell Motion tracking.
|
||||
disableMouseCellMotion()
|
||||
|
||||
// enableMouseAllMotion enables mouse click, release, wheel and motion
|
||||
// events, regardless of whether a mouse button is pressed. Many modern
|
||||
// terminals support this, but not all.
|
||||
enableMouseAllMotion()
|
||||
|
||||
// disableMouseAllMotion disables All Motion mouse tracking.
|
||||
disableMouseAllMotion()
|
||||
|
||||
// enableMouseSGRMode enables mouse extended mode (SGR).
|
||||
enableMouseSGRMode()
|
||||
|
||||
// disableMouseSGRMode disables mouse extended mode (SGR).
|
||||
disableMouseSGRMode()
|
||||
|
||||
// enableBracketedPaste enables bracketed paste, where characters
|
||||
// inside the input are not interpreted when pasted as a whole.
|
||||
enableBracketedPaste()
|
||||
|
||||
// disableBracketedPaste disables bracketed paste.
|
||||
disableBracketedPaste()
|
||||
|
||||
// bracketedPasteActive reports whether bracketed paste mode is
|
||||
// currently enabled.
|
||||
bracketedPasteActive() bool
|
||||
|
||||
// setWindowTitle sets the terminal window title.
|
||||
setWindowTitle(string)
|
||||
|
||||
// reportFocus returns whether reporting focus events is enabled.
|
||||
reportFocus() bool
|
||||
|
||||
// enableReportFocus reports focus events to the program.
|
||||
enableReportFocus()
|
||||
|
||||
// disableReportFocus stops reporting focus events to the program.
|
||||
disableReportFocus()
|
||||
}
|
||||
|
||||
// repaintMsg forces a full repaint.
|
||||
type repaintMsg struct{}
|
248
vendor/github.com/charmbracelet/bubbletea/screen.go
generated
vendored
Normal file
248
vendor/github.com/charmbracelet/bubbletea/screen.go
generated
vendored
Normal file
@ -0,0 +1,248 @@
|
||||
package tea
|
||||
|
||||
// WindowSizeMsg is used to report the terminal size. It's sent to Update once
|
||||
// initially and then on every terminal resize. Note that Windows does not
|
||||
// have support for reporting when resizes occur as it does not support the
|
||||
// SIGWINCH signal.
|
||||
type WindowSizeMsg struct {
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
// ClearScreen is a special command that tells the program to clear the screen
|
||||
// before the next update. This can be used to move the cursor to the top left
|
||||
// of the screen and clear visual clutter when the alt screen is not in use.
|
||||
//
|
||||
// Note that it should never be necessary to call ClearScreen() for regular
|
||||
// redraws.
|
||||
func ClearScreen() Msg {
|
||||
return clearScreenMsg{}
|
||||
}
|
||||
|
||||
// clearScreenMsg is an internal message that signals to clear the screen.
|
||||
// You can send a clearScreenMsg with ClearScreen.
|
||||
type clearScreenMsg struct{}
|
||||
|
||||
// EnterAltScreen is a special command that tells the Bubble Tea program to
|
||||
// enter the alternate screen buffer.
|
||||
//
|
||||
// Because commands run asynchronously, this command should not be used in your
|
||||
// model's Init function. To initialize your program with the altscreen enabled
|
||||
// use the WithAltScreen ProgramOption instead.
|
||||
func EnterAltScreen() Msg {
|
||||
return enterAltScreenMsg{}
|
||||
}
|
||||
|
||||
// enterAltScreenMsg in an internal message signals that the program should
|
||||
// enter alternate screen buffer. You can send a enterAltScreenMsg with
|
||||
// EnterAltScreen.
|
||||
type enterAltScreenMsg struct{}
|
||||
|
||||
// ExitAltScreen is a special command that tells the Bubble Tea program to exit
|
||||
// the alternate screen buffer. This command should be used to exit the
|
||||
// alternate screen buffer while the program is running.
|
||||
//
|
||||
// Note that the alternate screen buffer will be automatically exited when the
|
||||
// program quits.
|
||||
func ExitAltScreen() Msg {
|
||||
return exitAltScreenMsg{}
|
||||
}
|
||||
|
||||
// exitAltScreenMsg in an internal message signals that the program should exit
|
||||
// alternate screen buffer. You can send a exitAltScreenMsg with ExitAltScreen.
|
||||
type exitAltScreenMsg struct{}
|
||||
|
||||
// EnableMouseCellMotion is a special command that enables mouse click,
|
||||
// release, and wheel events. Mouse movement events are also captured if
|
||||
// a mouse button is pressed (i.e., drag events).
|
||||
//
|
||||
// Because commands run asynchronously, this command should not be used in your
|
||||
// model's Init function. Use the WithMouseCellMotion ProgramOption instead.
|
||||
func EnableMouseCellMotion() Msg {
|
||||
return enableMouseCellMotionMsg{}
|
||||
}
|
||||
|
||||
// enableMouseCellMotionMsg is a special command that signals to start
|
||||
// listening for "cell motion" type mouse events (ESC[?1002l). To send an
|
||||
// enableMouseCellMotionMsg, use the EnableMouseCellMotion command.
|
||||
type enableMouseCellMotionMsg struct{}
|
||||
|
||||
// EnableMouseAllMotion is a special command that enables mouse click, release,
|
||||
// wheel, and motion events, which are delivered regardless of whether a mouse
|
||||
// button is pressed, effectively enabling support for hover interactions.
|
||||
//
|
||||
// Many modern terminals support this, but not all. If in doubt, use
|
||||
// EnableMouseCellMotion instead.
|
||||
//
|
||||
// Because commands run asynchronously, this command should not be used in your
|
||||
// model's Init function. Use the WithMouseAllMotion ProgramOption instead.
|
||||
func EnableMouseAllMotion() Msg {
|
||||
return enableMouseAllMotionMsg{}
|
||||
}
|
||||
|
||||
// enableMouseAllMotionMsg is a special command that signals to start listening
|
||||
// for "all motion" type mouse events (ESC[?1003l). To send an
|
||||
// enableMouseAllMotionMsg, use the EnableMouseAllMotion command.
|
||||
type enableMouseAllMotionMsg struct{}
|
||||
|
||||
// DisableMouse is a special command that stops listening for mouse events.
|
||||
func DisableMouse() Msg {
|
||||
return disableMouseMsg{}
|
||||
}
|
||||
|
||||
// disableMouseMsg is an internal message that signals to stop listening
|
||||
// for mouse events. To send a disableMouseMsg, use the DisableMouse command.
|
||||
type disableMouseMsg struct{}
|
||||
|
||||
// HideCursor is a special command for manually instructing Bubble Tea to hide
|
||||
// the cursor. In some rare cases, certain operations will cause the terminal
|
||||
// to show the cursor, which is normally hidden for the duration of a Bubble
|
||||
// Tea program's lifetime. You will most likely not need to use this command.
|
||||
func HideCursor() Msg {
|
||||
return hideCursorMsg{}
|
||||
}
|
||||
|
||||
// hideCursorMsg is an internal command used to hide the cursor. You can send
|
||||
// this message with HideCursor.
|
||||
type hideCursorMsg struct{}
|
||||
|
||||
// ShowCursor is a special command for manually instructing Bubble Tea to show
|
||||
// the cursor.
|
||||
func ShowCursor() Msg {
|
||||
return showCursorMsg{}
|
||||
}
|
||||
|
||||
// showCursorMsg is an internal command used to show the cursor. You can send
|
||||
// this message with ShowCursor.
|
||||
type showCursorMsg struct{}
|
||||
|
||||
// EnableBracketedPaste is a special command that tells the Bubble Tea program
|
||||
// to accept bracketed paste input.
|
||||
//
|
||||
// Note that bracketed paste will be automatically disabled when the
|
||||
// program quits.
|
||||
func EnableBracketedPaste() Msg {
|
||||
return enableBracketedPasteMsg{}
|
||||
}
|
||||
|
||||
// enableBracketedPasteMsg in an internal message signals that
|
||||
// bracketed paste should be enabled. You can send an
|
||||
// enableBracketedPasteMsg with EnableBracketedPaste.
|
||||
type enableBracketedPasteMsg struct{}
|
||||
|
||||
// DisableBracketedPaste is a special command that tells the Bubble Tea program
|
||||
// to accept bracketed paste input.
|
||||
//
|
||||
// Note that bracketed paste will be automatically disabled when the
|
||||
// program quits.
|
||||
func DisableBracketedPaste() Msg {
|
||||
return disableBracketedPasteMsg{}
|
||||
}
|
||||
|
||||
// disableBracketedPasteMsg in an internal message signals that
|
||||
// bracketed paste should be disabled. You can send an
|
||||
// disableBracketedPasteMsg with DisableBracketedPaste.
|
||||
type disableBracketedPasteMsg struct{}
|
||||
|
||||
// enableReportFocusMsg is an internal message that signals to enable focus
|
||||
// reporting. You can send an enableReportFocusMsg with EnableReportFocus.
|
||||
type enableReportFocusMsg struct{}
|
||||
|
||||
// EnableReportFocus is a special command that tells the Bubble Tea program to
|
||||
// report focus events to the program.
|
||||
func EnableReportFocus() Msg {
|
||||
return enableReportFocusMsg{}
|
||||
}
|
||||
|
||||
// disableReportFocusMsg is an internal message that signals to disable focus
|
||||
// reporting. You can send an disableReportFocusMsg with DisableReportFocus.
|
||||
type disableReportFocusMsg struct{}
|
||||
|
||||
// DisableReportFocus is a special command that tells the Bubble Tea program to
|
||||
// stop reporting focus events to the program.
|
||||
func DisableReportFocus() Msg {
|
||||
return disableReportFocusMsg{}
|
||||
}
|
||||
|
||||
// EnterAltScreen enters the alternate screen buffer, which consumes the entire
|
||||
// terminal window. ExitAltScreen will return the terminal to its former state.
|
||||
//
|
||||
// Deprecated: Use the WithAltScreen ProgramOption instead.
|
||||
func (p *Program) EnterAltScreen() {
|
||||
if p.renderer != nil {
|
||||
p.renderer.enterAltScreen()
|
||||
} else {
|
||||
p.startupOptions |= withAltScreen
|
||||
}
|
||||
}
|
||||
|
||||
// ExitAltScreen exits the alternate screen buffer.
|
||||
//
|
||||
// Deprecated: The altscreen will exited automatically when the program exits.
|
||||
func (p *Program) ExitAltScreen() {
|
||||
if p.renderer != nil {
|
||||
p.renderer.exitAltScreen()
|
||||
} else {
|
||||
p.startupOptions &^= withAltScreen
|
||||
}
|
||||
}
|
||||
|
||||
// EnableMouseCellMotion enables mouse click, release, wheel and motion events
|
||||
// if a mouse button is pressed (i.e., drag events).
|
||||
//
|
||||
// Deprecated: Use the WithMouseCellMotion ProgramOption instead.
|
||||
func (p *Program) EnableMouseCellMotion() {
|
||||
if p.renderer != nil {
|
||||
p.renderer.enableMouseCellMotion()
|
||||
} else {
|
||||
p.startupOptions |= withMouseCellMotion
|
||||
}
|
||||
}
|
||||
|
||||
// DisableMouseCellMotion disables Mouse Cell Motion tracking. This will be
|
||||
// called automatically when exiting a Bubble Tea program.
|
||||
//
|
||||
// Deprecated: The mouse will automatically be disabled when the program exits.
|
||||
func (p *Program) DisableMouseCellMotion() {
|
||||
if p.renderer != nil {
|
||||
p.renderer.disableMouseCellMotion()
|
||||
} else {
|
||||
p.startupOptions &^= withMouseCellMotion
|
||||
}
|
||||
}
|
||||
|
||||
// EnableMouseAllMotion enables mouse click, release, wheel and motion events,
|
||||
// regardless of whether a mouse button is pressed. Many modern terminals
|
||||
// support this, but not all.
|
||||
//
|
||||
// Deprecated: Use the WithMouseAllMotion ProgramOption instead.
|
||||
func (p *Program) EnableMouseAllMotion() {
|
||||
if p.renderer != nil {
|
||||
p.renderer.enableMouseAllMotion()
|
||||
} else {
|
||||
p.startupOptions |= withMouseAllMotion
|
||||
}
|
||||
}
|
||||
|
||||
// DisableMouseAllMotion disables All Motion mouse tracking. This will be
|
||||
// called automatically when exiting a Bubble Tea program.
|
||||
//
|
||||
// Deprecated: The mouse will automatically be disabled when the program exits.
|
||||
func (p *Program) DisableMouseAllMotion() {
|
||||
if p.renderer != nil {
|
||||
p.renderer.disableMouseAllMotion()
|
||||
} else {
|
||||
p.startupOptions &^= withMouseAllMotion
|
||||
}
|
||||
}
|
||||
|
||||
// SetWindowTitle sets the terminal window title.
|
||||
//
|
||||
// Deprecated: Use the SetWindowTitle command instead.
|
||||
func (p *Program) SetWindowTitle(title string) {
|
||||
if p.renderer != nil {
|
||||
p.renderer.setWindowTitle(title)
|
||||
} else {
|
||||
p.startupTitle = title
|
||||
}
|
||||
}
|
33
vendor/github.com/charmbracelet/bubbletea/signals_unix.go
generated
vendored
Normal file
33
vendor/github.com/charmbracelet/bubbletea/signals_unix.go
generated
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || aix || zos
|
||||
// +build darwin dragonfly freebsd linux netbsd openbsd solaris aix zos
|
||||
|
||||
package tea
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// listenForResize sends messages (or errors) when the terminal resizes.
|
||||
// Argument output should be the file descriptor for the terminal; usually
|
||||
// os.Stdout.
|
||||
func (p *Program) listenForResize(done chan struct{}) {
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, syscall.SIGWINCH)
|
||||
|
||||
defer func() {
|
||||
signal.Stop(sig)
|
||||
close(done)
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-p.ctx.Done():
|
||||
return
|
||||
case <-sig:
|
||||
}
|
||||
|
||||
p.checkResize()
|
||||
}
|
||||
}
|
10
vendor/github.com/charmbracelet/bubbletea/signals_windows.go
generated
vendored
Normal file
10
vendor/github.com/charmbracelet/bubbletea/signals_windows.go
generated
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package tea
|
||||
|
||||
// listenForResize is not available on windows because windows does not
|
||||
// implement syscall.SIGWINCH.
|
||||
func (p *Program) listenForResize(done chan struct{}) {
|
||||
close(done)
|
||||
}
|
786
vendor/github.com/charmbracelet/bubbletea/standard_renderer.go
generated
vendored
Normal file
786
vendor/github.com/charmbracelet/bubbletea/standard_renderer.go
generated
vendored
Normal file
@ -0,0 +1,786 @@
|
||||
package tea
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
"github.com/muesli/ansi/compressor"
|
||||
)
|
||||
|
||||
const (
|
||||
// defaultFramerate specifies the maximum interval at which we should
|
||||
// update the view.
|
||||
defaultFPS = 60
|
||||
maxFPS = 120
|
||||
)
|
||||
|
||||
// standardRenderer is a framerate-based terminal renderer, updating the view
|
||||
// at a given framerate to avoid overloading the terminal emulator.
|
||||
//
|
||||
// In cases where very high performance is needed the renderer can be told
|
||||
// to exclude ranges of lines, allowing them to be written to directly.
|
||||
type standardRenderer struct {
|
||||
mtx *sync.Mutex
|
||||
out io.Writer
|
||||
|
||||
buf bytes.Buffer
|
||||
queuedMessageLines []string
|
||||
framerate time.Duration
|
||||
ticker *time.Ticker
|
||||
done chan struct{}
|
||||
lastRender string
|
||||
lastRenderedLines []string
|
||||
linesRendered int
|
||||
altLinesRendered int
|
||||
useANSICompressor bool
|
||||
once sync.Once
|
||||
|
||||
// cursor visibility state
|
||||
cursorHidden bool
|
||||
|
||||
// essentially whether or not we're using the full size of the terminal
|
||||
altScreenActive bool
|
||||
|
||||
// whether or not we're currently using bracketed paste
|
||||
bpActive bool
|
||||
|
||||
// reportingFocus whether reporting focus events is enabled
|
||||
reportingFocus bool
|
||||
|
||||
// renderer dimensions; usually the size of the window
|
||||
width int
|
||||
height int
|
||||
|
||||
// lines explicitly set not to render
|
||||
ignoreLines map[int]struct{}
|
||||
}
|
||||
|
||||
// newRenderer creates a new renderer. Normally you'll want to initialize it
|
||||
// with os.Stdout as the first argument.
|
||||
func newRenderer(out io.Writer, useANSICompressor bool, fps int) renderer {
|
||||
if fps < 1 {
|
||||
fps = defaultFPS
|
||||
} else if fps > maxFPS {
|
||||
fps = maxFPS
|
||||
}
|
||||
r := &standardRenderer{
|
||||
out: out,
|
||||
mtx: &sync.Mutex{},
|
||||
done: make(chan struct{}),
|
||||
framerate: time.Second / time.Duration(fps),
|
||||
useANSICompressor: useANSICompressor,
|
||||
queuedMessageLines: []string{},
|
||||
}
|
||||
if r.useANSICompressor {
|
||||
r.out = &compressor.Writer{Forward: out}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// start starts the renderer.
|
||||
func (r *standardRenderer) start() {
|
||||
if r.ticker == nil {
|
||||
r.ticker = time.NewTicker(r.framerate)
|
||||
} else {
|
||||
// If the ticker already exists, it has been stopped and we need to
|
||||
// reset it.
|
||||
r.ticker.Reset(r.framerate)
|
||||
}
|
||||
|
||||
// Since the renderer can be restarted after a stop, we need to reset
|
||||
// the done channel and its corresponding sync.Once.
|
||||
r.once = sync.Once{}
|
||||
|
||||
go r.listen()
|
||||
}
|
||||
|
||||
// stop permanently halts the renderer, rendering the final frame.
|
||||
func (r *standardRenderer) stop() {
|
||||
// Stop the renderer before acquiring the mutex to avoid a deadlock.
|
||||
r.once.Do(func() {
|
||||
r.done <- struct{}{}
|
||||
})
|
||||
|
||||
// flush locks the mutex
|
||||
r.flush()
|
||||
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
|
||||
r.execute(ansi.EraseEntireLine)
|
||||
// Move the cursor back to the beginning of the line
|
||||
r.execute("\r")
|
||||
|
||||
if r.useANSICompressor {
|
||||
if w, ok := r.out.(io.WriteCloser); ok {
|
||||
_ = w.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// execute writes a sequence to the terminal.
|
||||
func (r *standardRenderer) execute(seq string) {
|
||||
_, _ = io.WriteString(r.out, seq)
|
||||
}
|
||||
|
||||
// kill halts the renderer. The final frame will not be rendered.
|
||||
func (r *standardRenderer) kill() {
|
||||
// Stop the renderer before acquiring the mutex to avoid a deadlock.
|
||||
r.once.Do(func() {
|
||||
r.done <- struct{}{}
|
||||
})
|
||||
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
|
||||
r.execute(ansi.EraseEntireLine)
|
||||
// Move the cursor back to the beginning of the line
|
||||
r.execute("\r")
|
||||
}
|
||||
|
||||
// listen waits for ticks on the ticker, or a signal to stop the renderer.
|
||||
func (r *standardRenderer) listen() {
|
||||
for {
|
||||
select {
|
||||
case <-r.done:
|
||||
r.ticker.Stop()
|
||||
return
|
||||
|
||||
case <-r.ticker.C:
|
||||
r.flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// flush renders the buffer.
|
||||
func (r *standardRenderer) flush() {
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
|
||||
if r.buf.Len() == 0 || r.buf.String() == r.lastRender {
|
||||
// Nothing to do.
|
||||
return
|
||||
}
|
||||
|
||||
// Output buffer.
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
// Moving to the beginning of the section, that we rendered.
|
||||
if r.altScreenActive {
|
||||
buf.WriteString(ansi.CursorHomePosition)
|
||||
} else if r.linesRendered > 1 {
|
||||
buf.WriteString(ansi.CursorUp(r.linesRendered - 1))
|
||||
}
|
||||
|
||||
newLines := strings.Split(r.buf.String(), "\n")
|
||||
|
||||
// If we know the output's height, we can use it to determine how many
|
||||
// lines we can render. We drop lines from the top of the render buffer if
|
||||
// necessary, as we can't navigate the cursor into the terminal's scrollback
|
||||
// buffer.
|
||||
if r.height > 0 && len(newLines) > r.height {
|
||||
newLines = newLines[len(newLines)-r.height:]
|
||||
}
|
||||
|
||||
flushQueuedMessages := len(r.queuedMessageLines) > 0 && !r.altScreenActive
|
||||
|
||||
if flushQueuedMessages {
|
||||
// Dump the lines we've queued up for printing.
|
||||
for _, line := range r.queuedMessageLines {
|
||||
if ansi.StringWidth(line) < r.width {
|
||||
// We only erase the rest of the line when the line is shorter than
|
||||
// the width of the terminal. When the cursor reaches the end of
|
||||
// the line, any escape sequences that follow will only affect the
|
||||
// last cell of the line.
|
||||
|
||||
// Removing previously rendered content at the end of line.
|
||||
line = line + ansi.EraseLineRight
|
||||
}
|
||||
|
||||
_, _ = buf.WriteString(line)
|
||||
_, _ = buf.WriteString("\r\n")
|
||||
}
|
||||
// Clear the queued message lines.
|
||||
r.queuedMessageLines = []string{}
|
||||
}
|
||||
|
||||
// Paint new lines.
|
||||
for i := 0; i < len(newLines); i++ {
|
||||
canSkip := !flushQueuedMessages && // Queuing messages triggers repaint -> we don't have access to previous frame content.
|
||||
len(r.lastRenderedLines) > i && r.lastRenderedLines[i] == newLines[i] // Previously rendered line is the same.
|
||||
|
||||
if _, ignore := r.ignoreLines[i]; ignore || canSkip {
|
||||
// Unless this is the last line, move the cursor down.
|
||||
if i < len(newLines)-1 {
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if i == 0 && r.lastRender == "" {
|
||||
// On first render, reset the cursor to the start of the line
|
||||
// before writing anything.
|
||||
buf.WriteByte('\r')
|
||||
}
|
||||
|
||||
line := newLines[i]
|
||||
|
||||
// Truncate lines wider than the width of the window to avoid
|
||||
// wrapping, which will mess up rendering. If we don't have the
|
||||
// width of the window this will be ignored.
|
||||
//
|
||||
// Note that on Windows we only get the width of the window on
|
||||
// program initialization, so after a resize this won't perform
|
||||
// correctly (signal SIGWINCH is not supported on Windows).
|
||||
if r.width > 0 {
|
||||
line = ansi.Truncate(line, r.width, "")
|
||||
}
|
||||
|
||||
if ansi.StringWidth(line) < r.width {
|
||||
// We only erase the rest of the line when the line is shorter than
|
||||
// the width of the terminal. When the cursor reaches the end of
|
||||
// the line, any escape sequences that follow will only affect the
|
||||
// last cell of the line.
|
||||
|
||||
// Removing previously rendered content at the end of line.
|
||||
line = line + ansi.EraseLineRight
|
||||
}
|
||||
|
||||
_, _ = buf.WriteString(line)
|
||||
|
||||
if i < len(newLines)-1 {
|
||||
_, _ = buf.WriteString("\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Clearing left over content from last render.
|
||||
if r.lastLinesRendered() > len(newLines) {
|
||||
buf.WriteString(ansi.EraseScreenBelow)
|
||||
}
|
||||
|
||||
if r.altScreenActive {
|
||||
r.altLinesRendered = len(newLines)
|
||||
} else {
|
||||
r.linesRendered = len(newLines)
|
||||
}
|
||||
|
||||
// Make sure the cursor is at the start of the last line to keep rendering
|
||||
// behavior consistent.
|
||||
if r.altScreenActive {
|
||||
// This case fixes a bug in macOS terminal. In other terminals the
|
||||
// other case seems to do the job regardless of whether or not we're
|
||||
// using the full terminal window.
|
||||
buf.WriteString(ansi.CursorPosition(0, len(newLines)))
|
||||
} else {
|
||||
buf.WriteString(ansi.CursorBackward(r.width))
|
||||
}
|
||||
|
||||
_, _ = r.out.Write(buf.Bytes())
|
||||
r.lastRender = r.buf.String()
|
||||
|
||||
// Save previously rendered lines for comparison in the next render. If we
|
||||
// don't do this, we can't skip rendering lines that haven't changed.
|
||||
// See https://github.com/charmbracelet/bubbletea/pull/1233
|
||||
r.lastRenderedLines = newLines
|
||||
r.buf.Reset()
|
||||
}
|
||||
|
||||
// lastLinesRendered returns the number of lines rendered lastly.
|
||||
func (r *standardRenderer) lastLinesRendered() int {
|
||||
if r.altScreenActive {
|
||||
return r.altLinesRendered
|
||||
}
|
||||
return r.linesRendered
|
||||
}
|
||||
|
||||
// write writes to the internal buffer. The buffer will be outputted via the
|
||||
// ticker which calls flush().
|
||||
func (r *standardRenderer) write(s string) {
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
r.buf.Reset()
|
||||
|
||||
// If an empty string was passed we should clear existing output and
|
||||
// rendering nothing. Rather than introduce additional state to manage
|
||||
// this, we render a single space as a simple (albeit less correct)
|
||||
// solution.
|
||||
if s == "" {
|
||||
s = " "
|
||||
}
|
||||
|
||||
_, _ = r.buf.WriteString(s)
|
||||
}
|
||||
|
||||
func (r *standardRenderer) repaint() {
|
||||
r.lastRender = ""
|
||||
r.lastRenderedLines = nil
|
||||
}
|
||||
|
||||
func (r *standardRenderer) clearScreen() {
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
|
||||
r.execute(ansi.EraseEntireScreen)
|
||||
r.execute(ansi.CursorHomePosition)
|
||||
|
||||
r.repaint()
|
||||
}
|
||||
|
||||
func (r *standardRenderer) altScreen() bool {
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
|
||||
return r.altScreenActive
|
||||
}
|
||||
|
||||
func (r *standardRenderer) enterAltScreen() {
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
|
||||
if r.altScreenActive {
|
||||
return
|
||||
}
|
||||
|
||||
r.altScreenActive = true
|
||||
r.execute(ansi.SetAltScreenSaveCursorMode)
|
||||
|
||||
// Ensure that the terminal is cleared, even when it doesn't support
|
||||
// alt screen (or alt screen support is disabled, like GNU screen by
|
||||
// default).
|
||||
//
|
||||
// Note: we can't use r.clearScreen() here because the mutex is already
|
||||
// locked.
|
||||
r.execute(ansi.EraseEntireScreen)
|
||||
r.execute(ansi.CursorHomePosition)
|
||||
|
||||
// cmd.exe and other terminals keep separate cursor states for the AltScreen
|
||||
// and the main buffer. We have to explicitly reset the cursor visibility
|
||||
// whenever we enter AltScreen.
|
||||
if r.cursorHidden {
|
||||
r.execute(ansi.HideCursor)
|
||||
} else {
|
||||
r.execute(ansi.ShowCursor)
|
||||
}
|
||||
|
||||
// Entering the alt screen resets the lines rendered count.
|
||||
r.altLinesRendered = 0
|
||||
|
||||
r.repaint()
|
||||
}
|
||||
|
||||
func (r *standardRenderer) exitAltScreen() {
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
|
||||
if !r.altScreenActive {
|
||||
return
|
||||
}
|
||||
|
||||
r.altScreenActive = false
|
||||
r.execute(ansi.ResetAltScreenSaveCursorMode)
|
||||
|
||||
// cmd.exe and other terminals keep separate cursor states for the AltScreen
|
||||
// and the main buffer. We have to explicitly reset the cursor visibility
|
||||
// whenever we exit AltScreen.
|
||||
if r.cursorHidden {
|
||||
r.execute(ansi.HideCursor)
|
||||
} else {
|
||||
r.execute(ansi.ShowCursor)
|
||||
}
|
||||
|
||||
r.repaint()
|
||||
}
|
||||
|
||||
func (r *standardRenderer) showCursor() {
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
|
||||
r.cursorHidden = false
|
||||
r.execute(ansi.ShowCursor)
|
||||
}
|
||||
|
||||
func (r *standardRenderer) hideCursor() {
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
|
||||
r.cursorHidden = true
|
||||
r.execute(ansi.HideCursor)
|
||||
}
|
||||
|
||||
func (r *standardRenderer) enableMouseCellMotion() {
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
|
||||
r.execute(ansi.SetButtonEventMouseMode)
|
||||
}
|
||||
|
||||
func (r *standardRenderer) disableMouseCellMotion() {
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
|
||||
r.execute(ansi.ResetButtonEventMouseMode)
|
||||
}
|
||||
|
||||
func (r *standardRenderer) enableMouseAllMotion() {
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
|
||||
r.execute(ansi.SetAnyEventMouseMode)
|
||||
}
|
||||
|
||||
func (r *standardRenderer) disableMouseAllMotion() {
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
|
||||
r.execute(ansi.ResetAnyEventMouseMode)
|
||||
}
|
||||
|
||||
func (r *standardRenderer) enableMouseSGRMode() {
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
|
||||
r.execute(ansi.SetSgrExtMouseMode)
|
||||
}
|
||||
|
||||
func (r *standardRenderer) disableMouseSGRMode() {
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
|
||||
r.execute(ansi.ResetSgrExtMouseMode)
|
||||
}
|
||||
|
||||
func (r *standardRenderer) enableBracketedPaste() {
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
|
||||
r.execute(ansi.SetBracketedPasteMode)
|
||||
r.bpActive = true
|
||||
}
|
||||
|
||||
func (r *standardRenderer) disableBracketedPaste() {
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
|
||||
r.execute(ansi.ResetBracketedPasteMode)
|
||||
r.bpActive = false
|
||||
}
|
||||
|
||||
func (r *standardRenderer) bracketedPasteActive() bool {
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
|
||||
return r.bpActive
|
||||
}
|
||||
|
||||
func (r *standardRenderer) enableReportFocus() {
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
|
||||
r.execute(ansi.SetFocusEventMode)
|
||||
r.reportingFocus = true
|
||||
}
|
||||
|
||||
func (r *standardRenderer) disableReportFocus() {
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
|
||||
r.execute(ansi.ResetFocusEventMode)
|
||||
r.reportingFocus = false
|
||||
}
|
||||
|
||||
func (r *standardRenderer) reportFocus() bool {
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
|
||||
return r.reportingFocus
|
||||
}
|
||||
|
||||
// setWindowTitle sets the terminal window title.
|
||||
func (r *standardRenderer) setWindowTitle(title string) {
|
||||
r.execute(ansi.SetWindowTitle(title))
|
||||
}
|
||||
|
||||
// setIgnoredLines specifies lines not to be touched by the standard Bubble Tea
|
||||
// renderer.
|
||||
func (r *standardRenderer) setIgnoredLines(from int, to int) {
|
||||
// Lock if we're going to be clearing some lines since we don't want
|
||||
// anything jacking our cursor.
|
||||
if r.lastLinesRendered() > 0 {
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
}
|
||||
|
||||
if r.ignoreLines == nil {
|
||||
r.ignoreLines = make(map[int]struct{})
|
||||
}
|
||||
for i := from; i < to; i++ {
|
||||
r.ignoreLines[i] = struct{}{}
|
||||
}
|
||||
|
||||
// Erase ignored lines
|
||||
lastLinesRendered := r.lastLinesRendered()
|
||||
if lastLinesRendered > 0 {
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
for i := lastLinesRendered - 1; i >= 0; i-- {
|
||||
if _, exists := r.ignoreLines[i]; exists {
|
||||
buf.WriteString(ansi.EraseEntireLine)
|
||||
}
|
||||
buf.WriteString(ansi.CUU1)
|
||||
}
|
||||
buf.WriteString(ansi.CursorPosition(0, lastLinesRendered)) // put cursor back
|
||||
_, _ = r.out.Write(buf.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
// clearIgnoredLines returns control of any ignored lines to the standard
|
||||
// Bubble Tea renderer. That is, any lines previously set to be ignored can be
|
||||
// rendered to again.
|
||||
func (r *standardRenderer) clearIgnoredLines() {
|
||||
r.ignoreLines = nil
|
||||
}
|
||||
|
||||
// insertTop effectively scrolls up. It inserts lines at the top of a given
|
||||
// area designated to be a scrollable region, pushing everything else down.
|
||||
// This is roughly how ncurses does it.
|
||||
//
|
||||
// To call this function use command ScrollUp().
|
||||
//
|
||||
// For this to work renderer.ignoreLines must be set to ignore the scrollable
|
||||
// region since we are bypassing the normal Bubble Tea renderer here.
|
||||
//
|
||||
// Because this method relies on the terminal dimensions, it's only valid for
|
||||
// full-window applications (generally those that use the alternate screen
|
||||
// buffer).
|
||||
//
|
||||
// This method bypasses the normal rendering buffer and is philosophically
|
||||
// different than the normal way we approach rendering in Bubble Tea. It's for
|
||||
// use in high-performance rendering, such as a pager that could potentially
|
||||
// be rendering very complicated ansi. In cases where the content is simpler
|
||||
// standard Bubble Tea rendering should suffice.
|
||||
//
|
||||
// Deprecated: This option is deprecated and will be removed in a future
|
||||
// version of this package.
|
||||
func (r *standardRenderer) insertTop(lines []string, topBoundary, bottomBoundary int) {
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
buf.WriteString(ansi.SetTopBottomMargins(topBoundary, bottomBoundary))
|
||||
buf.WriteString(ansi.CursorPosition(0, topBoundary))
|
||||
buf.WriteString(ansi.InsertLine(len(lines)))
|
||||
_, _ = buf.WriteString(strings.Join(lines, "\r\n"))
|
||||
buf.WriteString(ansi.SetTopBottomMargins(0, r.height))
|
||||
|
||||
// Move cursor back to where the main rendering routine expects it to be
|
||||
buf.WriteString(ansi.CursorPosition(0, r.lastLinesRendered()))
|
||||
|
||||
_, _ = r.out.Write(buf.Bytes())
|
||||
}
|
||||
|
||||
// insertBottom effectively scrolls down. It inserts lines at the bottom of
|
||||
// a given area designated to be a scrollable region, pushing everything else
|
||||
// up. This is roughly how ncurses does it.
|
||||
//
|
||||
// To call this function use the command ScrollDown().
|
||||
//
|
||||
// See note in insertTop() for caveats, how this function only makes sense for
|
||||
// full-window applications, and how it differs from the normal way we do
|
||||
// rendering in Bubble Tea.
|
||||
//
|
||||
// Deprecated: This option is deprecated and will be removed in a future
|
||||
// version of this package.
|
||||
func (r *standardRenderer) insertBottom(lines []string, topBoundary, bottomBoundary int) {
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
buf.WriteString(ansi.SetTopBottomMargins(topBoundary, bottomBoundary))
|
||||
buf.WriteString(ansi.CursorPosition(0, bottomBoundary))
|
||||
_, _ = buf.WriteString("\r\n" + strings.Join(lines, "\r\n"))
|
||||
buf.WriteString(ansi.SetTopBottomMargins(0, r.height))
|
||||
|
||||
// Move cursor back to where the main rendering routine expects it to be
|
||||
buf.WriteString(ansi.CursorPosition(0, r.lastLinesRendered()))
|
||||
|
||||
_, _ = r.out.Write(buf.Bytes())
|
||||
}
|
||||
|
||||
// handleMessages handles internal messages for the renderer.
|
||||
func (r *standardRenderer) handleMessages(msg Msg) {
|
||||
switch msg := msg.(type) {
|
||||
case repaintMsg:
|
||||
// Force a repaint by clearing the render cache as we slide into a
|
||||
// render.
|
||||
r.mtx.Lock()
|
||||
r.repaint()
|
||||
r.mtx.Unlock()
|
||||
|
||||
case WindowSizeMsg:
|
||||
r.mtx.Lock()
|
||||
r.width = msg.Width
|
||||
r.height = msg.Height
|
||||
r.repaint()
|
||||
r.mtx.Unlock()
|
||||
|
||||
case clearScrollAreaMsg:
|
||||
r.clearIgnoredLines()
|
||||
|
||||
// Force a repaint on the area where the scrollable stuff was in this
|
||||
// update cycle
|
||||
r.mtx.Lock()
|
||||
r.repaint()
|
||||
r.mtx.Unlock()
|
||||
|
||||
case syncScrollAreaMsg:
|
||||
// Re-render scrolling area
|
||||
r.clearIgnoredLines()
|
||||
r.setIgnoredLines(msg.topBoundary, msg.bottomBoundary)
|
||||
r.insertTop(msg.lines, msg.topBoundary, msg.bottomBoundary)
|
||||
|
||||
// Force non-scrolling stuff to repaint in this update cycle
|
||||
r.mtx.Lock()
|
||||
r.repaint()
|
||||
r.mtx.Unlock()
|
||||
|
||||
case scrollUpMsg:
|
||||
r.insertTop(msg.lines, msg.topBoundary, msg.bottomBoundary)
|
||||
|
||||
case scrollDownMsg:
|
||||
r.insertBottom(msg.lines, msg.topBoundary, msg.bottomBoundary)
|
||||
|
||||
case printLineMessage:
|
||||
if !r.altScreenActive {
|
||||
lines := strings.Split(msg.messageBody, "\n")
|
||||
r.mtx.Lock()
|
||||
r.queuedMessageLines = append(r.queuedMessageLines, lines...)
|
||||
r.repaint()
|
||||
r.mtx.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HIGH-PERFORMANCE RENDERING STUFF
|
||||
|
||||
type syncScrollAreaMsg struct {
|
||||
lines []string
|
||||
topBoundary int
|
||||
bottomBoundary int
|
||||
}
|
||||
|
||||
// SyncScrollArea performs a paint of the entire region designated to be the
|
||||
// scrollable area. This is required to initialize the scrollable region and
|
||||
// should also be called on resize (WindowSizeMsg).
|
||||
//
|
||||
// For high-performance, scroll-based rendering only.
|
||||
//
|
||||
// Deprecated: This option will be removed in a future version of this package.
|
||||
func SyncScrollArea(lines []string, topBoundary int, bottomBoundary int) Cmd {
|
||||
return func() Msg {
|
||||
return syncScrollAreaMsg{
|
||||
lines: lines,
|
||||
topBoundary: topBoundary,
|
||||
bottomBoundary: bottomBoundary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type clearScrollAreaMsg struct{}
|
||||
|
||||
// ClearScrollArea deallocates the scrollable region and returns the control of
|
||||
// those lines to the main rendering routine.
|
||||
//
|
||||
// For high-performance, scroll-based rendering only.
|
||||
//
|
||||
// Deprecated: This option will be removed in a future version of this package.
|
||||
func ClearScrollArea() Msg {
|
||||
return clearScrollAreaMsg{}
|
||||
}
|
||||
|
||||
type scrollUpMsg struct {
|
||||
lines []string
|
||||
topBoundary int
|
||||
bottomBoundary int
|
||||
}
|
||||
|
||||
// ScrollUp adds lines to the top of the scrollable region, pushing existing
|
||||
// lines below down. Lines that are pushed out the scrollable region disappear
|
||||
// from view.
|
||||
//
|
||||
// For high-performance, scroll-based rendering only.
|
||||
//
|
||||
// Deprecated: This option will be removed in a future version of this package.
|
||||
func ScrollUp(newLines []string, topBoundary, bottomBoundary int) Cmd {
|
||||
return func() Msg {
|
||||
return scrollUpMsg{
|
||||
lines: newLines,
|
||||
topBoundary: topBoundary,
|
||||
bottomBoundary: bottomBoundary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type scrollDownMsg struct {
|
||||
lines []string
|
||||
topBoundary int
|
||||
bottomBoundary int
|
||||
}
|
||||
|
||||
// ScrollDown adds lines to the bottom of the scrollable region, pushing
|
||||
// existing lines above up. Lines that are pushed out of the scrollable region
|
||||
// disappear from view.
|
||||
//
|
||||
// For high-performance, scroll-based rendering only.
|
||||
//
|
||||
// Deprecated: This option will be removed in a future version of this package.
|
||||
func ScrollDown(newLines []string, topBoundary, bottomBoundary int) Cmd {
|
||||
return func() Msg {
|
||||
return scrollDownMsg{
|
||||
lines: newLines,
|
||||
topBoundary: topBoundary,
|
||||
bottomBoundary: bottomBoundary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type printLineMessage struct {
|
||||
messageBody string
|
||||
}
|
||||
|
||||
// Println prints above the Program. This output is unmanaged by the program and
|
||||
// will persist across renders by the Program.
|
||||
//
|
||||
// Unlike fmt.Println (but similar to log.Println) the message will be print on
|
||||
// its own line.
|
||||
//
|
||||
// If the altscreen is active no output will be printed.
|
||||
func Println(args ...interface{}) Cmd {
|
||||
return func() Msg {
|
||||
return printLineMessage{
|
||||
messageBody: fmt.Sprint(args...),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Printf prints above the Program. It takes a format template followed by
|
||||
// values similar to fmt.Printf. This output is unmanaged by the program and
|
||||
// will persist across renders by the Program.
|
||||
//
|
||||
// Unlike fmt.Printf (but similar to log.Printf) the message will be print on
|
||||
// its own line.
|
||||
//
|
||||
// If the altscreen is active no output will be printed.
|
||||
func Printf(template string, args ...interface{}) Cmd {
|
||||
return func() Msg {
|
||||
return printLineMessage{
|
||||
messageBody: fmt.Sprintf(template, args...),
|
||||
}
|
||||
}
|
||||
}
|
841
vendor/github.com/charmbracelet/bubbletea/tea.go
generated
vendored
Normal file
841
vendor/github.com/charmbracelet/bubbletea/tea.go
generated
vendored
Normal file
@ -0,0 +1,841 @@
|
||||
// Package tea provides a framework for building rich terminal user interfaces
|
||||
// based on the paradigms of The Elm Architecture. It's well-suited for simple
|
||||
// and complex terminal applications, either inline, full-window, or a mix of
|
||||
// both. It's been battle-tested in several large projects and is
|
||||
// production-ready.
|
||||
//
|
||||
// A tutorial is available at https://github.com/charmbracelet/bubbletea/tree/master/tutorials
|
||||
//
|
||||
// Example programs can be found at https://github.com/charmbracelet/bubbletea/tree/master/examples
|
||||
package tea
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
|
||||
"github.com/charmbracelet/x/term"
|
||||
"github.com/muesli/cancelreader"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// ErrProgramKilled is returned by [Program.Run] when the program gets killed.
|
||||
var ErrProgramKilled = errors.New("program was killed")
|
||||
|
||||
// ErrInterrupted is returned by [Program.Run] when the program get a SIGINT
|
||||
// signal, or when it receives a [InterruptMsg].
|
||||
var ErrInterrupted = errors.New("program was interrupted")
|
||||
|
||||
// Msg contain data from the result of a IO operation. Msgs trigger the update
|
||||
// function and, henceforth, the UI.
|
||||
type Msg interface{}
|
||||
|
||||
// Model contains the program's state as well as its core functions.
|
||||
type Model interface {
|
||||
// Init is the first function that will be called. It returns an optional
|
||||
// initial command. To not perform an initial command return nil.
|
||||
Init() Cmd
|
||||
|
||||
// Update is called when a message is received. Use it to inspect messages
|
||||
// and, in response, update the model and/or send a command.
|
||||
Update(Msg) (Model, Cmd)
|
||||
|
||||
// View renders the program's UI, which is just a string. The view is
|
||||
// rendered after every Update.
|
||||
View() string
|
||||
}
|
||||
|
||||
// Cmd is an IO operation that returns a message when it's complete. If it's
|
||||
// nil it's considered a no-op. Use it for things like HTTP requests, timers,
|
||||
// saving and loading from disk, and so on.
|
||||
//
|
||||
// Note that there's almost never a reason to use a command to send a message
|
||||
// to another part of your program. That can almost always be done in the
|
||||
// update function.
|
||||
type Cmd func() Msg
|
||||
|
||||
type inputType int
|
||||
|
||||
const (
|
||||
defaultInput inputType = iota
|
||||
ttyInput
|
||||
customInput
|
||||
)
|
||||
|
||||
// String implements the stringer interface for [inputType]. It is inteded to
|
||||
// be used in testing.
|
||||
func (i inputType) String() string {
|
||||
return [...]string{
|
||||
"default input",
|
||||
"tty input",
|
||||
"custom input",
|
||||
}[i]
|
||||
}
|
||||
|
||||
// Options to customize the program during its initialization. These are
|
||||
// generally set with ProgramOptions.
|
||||
//
|
||||
// The options here are treated as bits.
|
||||
type startupOptions int16
|
||||
|
||||
func (s startupOptions) has(option startupOptions) bool {
|
||||
return s&option != 0
|
||||
}
|
||||
|
||||
const (
|
||||
withAltScreen startupOptions = 1 << iota
|
||||
withMouseCellMotion
|
||||
withMouseAllMotion
|
||||
withANSICompressor
|
||||
withoutSignalHandler
|
||||
// Catching panics is incredibly useful for restoring the terminal to a
|
||||
// usable state after a panic occurs. When this is set, Bubble Tea will
|
||||
// recover from panics, print the stack trace, and disable raw mode. This
|
||||
// feature is on by default.
|
||||
withoutCatchPanics
|
||||
withoutBracketedPaste
|
||||
withReportFocus
|
||||
)
|
||||
|
||||
// channelHandlers manages the series of channels returned by various processes.
|
||||
// It allows us to wait for those processes to terminate before exiting the
|
||||
// program.
|
||||
type channelHandlers []chan struct{}
|
||||
|
||||
// Adds a channel to the list of handlers. We wait for all handlers to terminate
|
||||
// gracefully on shutdown.
|
||||
func (h *channelHandlers) add(ch chan struct{}) {
|
||||
*h = append(*h, ch)
|
||||
}
|
||||
|
||||
// shutdown waits for all handlers to terminate.
|
||||
func (h channelHandlers) shutdown() {
|
||||
var wg sync.WaitGroup
|
||||
for _, ch := range h {
|
||||
wg.Add(1)
|
||||
go func(ch chan struct{}) {
|
||||
<-ch
|
||||
wg.Done()
|
||||
}(ch)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// Program is a terminal user interface.
|
||||
type Program struct {
|
||||
initialModel Model
|
||||
|
||||
// handlers is a list of channels that need to be waited on before the
|
||||
// program can exit.
|
||||
handlers channelHandlers
|
||||
|
||||
// Configuration options that will set as the program is initializing,
|
||||
// treated as bits. These options can be set via various ProgramOptions.
|
||||
startupOptions startupOptions
|
||||
|
||||
// startupTitle is the title that will be set on the terminal when the
|
||||
// program starts.
|
||||
startupTitle string
|
||||
|
||||
inputType inputType
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
msgs chan Msg
|
||||
errs chan error
|
||||
finished chan struct{}
|
||||
|
||||
// where to send output, this will usually be os.Stdout.
|
||||
output io.Writer
|
||||
// ttyOutput is null if output is not a TTY.
|
||||
ttyOutput term.File
|
||||
previousOutputState *term.State
|
||||
renderer renderer
|
||||
|
||||
// the environment variables for the program, defaults to os.Environ().
|
||||
environ []string
|
||||
|
||||
// where to read inputs from, this will usually be os.Stdin.
|
||||
input io.Reader
|
||||
// ttyInput is null if input is not a TTY.
|
||||
ttyInput term.File
|
||||
previousTtyInputState *term.State
|
||||
cancelReader cancelreader.CancelReader
|
||||
readLoopDone chan struct{}
|
||||
|
||||
// was the altscreen active before releasing the terminal?
|
||||
altScreenWasActive bool
|
||||
ignoreSignals uint32
|
||||
|
||||
bpWasActive bool // was the bracketed paste mode active before releasing the terminal?
|
||||
reportFocus bool // was focus reporting active before releasing the terminal?
|
||||
|
||||
filter func(Model, Msg) Msg
|
||||
|
||||
// fps is the frames per second we should set on the renderer, if
|
||||
// applicable,
|
||||
fps int
|
||||
|
||||
// mouseMode is true if the program should enable mouse mode on Windows.
|
||||
mouseMode bool
|
||||
}
|
||||
|
||||
// Quit is a special command that tells the Bubble Tea program to exit.
|
||||
func Quit() Msg {
|
||||
return QuitMsg{}
|
||||
}
|
||||
|
||||
// QuitMsg signals that the program should quit. You can send a [QuitMsg] with
|
||||
// [Quit].
|
||||
type QuitMsg struct{}
|
||||
|
||||
// Suspend is a special command that tells the Bubble Tea program to suspend.
|
||||
func Suspend() Msg {
|
||||
return SuspendMsg{}
|
||||
}
|
||||
|
||||
// SuspendMsg signals the program should suspend.
|
||||
// This usually happens when ctrl+z is pressed on common programs, but since
|
||||
// bubbletea puts the terminal in raw mode, we need to handle it in a
|
||||
// per-program basis.
|
||||
//
|
||||
// You can send this message with [Suspend()].
|
||||
type SuspendMsg struct{}
|
||||
|
||||
// ResumeMsg can be listen to to do something once a program is resumed back
|
||||
// from a suspend state.
|
||||
type ResumeMsg struct{}
|
||||
|
||||
// InterruptMsg signals the program should suspend.
|
||||
// This usually happens when ctrl+c is pressed on common programs, but since
|
||||
// bubbletea puts the terminal in raw mode, we need to handle it in a
|
||||
// per-program basis.
|
||||
//
|
||||
// You can send this message with [Interrupt()].
|
||||
type InterruptMsg struct{}
|
||||
|
||||
// Interrupt is a special command that tells the Bubble Tea program to
|
||||
// interrupt.
|
||||
func Interrupt() Msg {
|
||||
return InterruptMsg{}
|
||||
}
|
||||
|
||||
// NewProgram creates a new Program.
|
||||
func NewProgram(model Model, opts ...ProgramOption) *Program {
|
||||
p := &Program{
|
||||
initialModel: model,
|
||||
msgs: make(chan Msg),
|
||||
}
|
||||
|
||||
// Apply all options to the program.
|
||||
for _, opt := range opts {
|
||||
opt(p)
|
||||
}
|
||||
|
||||
// A context can be provided with a ProgramOption, but if none was provided
|
||||
// we'll use the default background context.
|
||||
if p.ctx == nil {
|
||||
p.ctx = context.Background()
|
||||
}
|
||||
// Initialize context and teardown channel.
|
||||
p.ctx, p.cancel = context.WithCancel(p.ctx)
|
||||
|
||||
// if no output was set, set it to stdout
|
||||
if p.output == nil {
|
||||
p.output = os.Stdout
|
||||
}
|
||||
|
||||
// if no environment was set, set it to os.Environ()
|
||||
if p.environ == nil {
|
||||
p.environ = os.Environ()
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *Program) handleSignals() chan struct{} {
|
||||
ch := make(chan struct{})
|
||||
|
||||
// Listen for SIGINT and SIGTERM.
|
||||
//
|
||||
// In most cases ^C will not send an interrupt because the terminal will be
|
||||
// in raw mode and ^C will be captured as a keystroke and sent along to
|
||||
// Program.Update as a KeyMsg. When input is not a TTY, however, ^C will be
|
||||
// caught here.
|
||||
//
|
||||
// SIGTERM is sent by unix utilities (like kill) to terminate a process.
|
||||
go func() {
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
||||
defer func() {
|
||||
signal.Stop(sig)
|
||||
close(ch)
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-p.ctx.Done():
|
||||
return
|
||||
|
||||
case s := <-sig:
|
||||
if atomic.LoadUint32(&p.ignoreSignals) == 0 {
|
||||
switch s {
|
||||
case syscall.SIGINT:
|
||||
p.msgs <- InterruptMsg{}
|
||||
default:
|
||||
p.msgs <- QuitMsg{}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
// handleResize handles terminal resize events.
|
||||
func (p *Program) handleResize() chan struct{} {
|
||||
ch := make(chan struct{})
|
||||
|
||||
if p.ttyOutput != nil {
|
||||
// Get the initial terminal size and send it to the program.
|
||||
go p.checkResize()
|
||||
|
||||
// Listen for window resizes.
|
||||
go p.listenForResize(ch)
|
||||
} else {
|
||||
close(ch)
|
||||
}
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
// handleCommands runs commands in a goroutine and sends the result to the
|
||||
// program's message channel.
|
||||
func (p *Program) handleCommands(cmds chan Cmd) chan struct{} {
|
||||
ch := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
defer close(ch)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-p.ctx.Done():
|
||||
return
|
||||
|
||||
case cmd := <-cmds:
|
||||
if cmd == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Don't wait on these goroutines, otherwise the shutdown
|
||||
// latency would get too large as a Cmd can run for some time
|
||||
// (e.g. tick commands that sleep for half a second). It's not
|
||||
// possible to cancel them so we'll have to leak the goroutine
|
||||
// until Cmd returns.
|
||||
go func() {
|
||||
// Recover from panics.
|
||||
if !p.startupOptions.has(withoutCatchPanics) {
|
||||
defer p.recoverFromPanic()
|
||||
}
|
||||
|
||||
msg := cmd() // this can be long.
|
||||
p.Send(msg)
|
||||
}()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
func (p *Program) disableMouse() {
|
||||
p.renderer.disableMouseCellMotion()
|
||||
p.renderer.disableMouseAllMotion()
|
||||
p.renderer.disableMouseSGRMode()
|
||||
}
|
||||
|
||||
// eventLoop is the central message loop. It receives and handles the default
|
||||
// Bubble Tea messages, update the model and triggers redraws.
|
||||
func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
|
||||
for {
|
||||
select {
|
||||
case <-p.ctx.Done():
|
||||
return model, nil
|
||||
|
||||
case err := <-p.errs:
|
||||
return model, err
|
||||
|
||||
case msg := <-p.msgs:
|
||||
// Filter messages.
|
||||
if p.filter != nil {
|
||||
msg = p.filter(model, msg)
|
||||
}
|
||||
if msg == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle special internal messages.
|
||||
switch msg := msg.(type) {
|
||||
case QuitMsg:
|
||||
return model, nil
|
||||
|
||||
case InterruptMsg:
|
||||
return model, ErrInterrupted
|
||||
|
||||
case SuspendMsg:
|
||||
if suspendSupported {
|
||||
p.suspend()
|
||||
}
|
||||
|
||||
case clearScreenMsg:
|
||||
p.renderer.clearScreen()
|
||||
|
||||
case enterAltScreenMsg:
|
||||
p.renderer.enterAltScreen()
|
||||
|
||||
case exitAltScreenMsg:
|
||||
p.renderer.exitAltScreen()
|
||||
|
||||
case enableMouseCellMotionMsg, enableMouseAllMotionMsg:
|
||||
switch msg.(type) {
|
||||
case enableMouseCellMotionMsg:
|
||||
p.renderer.enableMouseCellMotion()
|
||||
case enableMouseAllMotionMsg:
|
||||
p.renderer.enableMouseAllMotion()
|
||||
}
|
||||
// mouse mode (1006) is a no-op if the terminal doesn't support it.
|
||||
p.renderer.enableMouseSGRMode()
|
||||
|
||||
// XXX: This is used to enable mouse mode on Windows. We need
|
||||
// to reinitialize the cancel reader to get the mouse events to
|
||||
// work.
|
||||
if runtime.GOOS == "windows" && !p.mouseMode {
|
||||
p.mouseMode = true
|
||||
p.initCancelReader(true) //nolint:errcheck
|
||||
}
|
||||
|
||||
case disableMouseMsg:
|
||||
p.disableMouse()
|
||||
|
||||
// XXX: On Windows, mouse mode is enabled on the input reader
|
||||
// level. We need to instruct the input reader to stop reading
|
||||
// mouse events.
|
||||
if runtime.GOOS == "windows" && p.mouseMode {
|
||||
p.mouseMode = false
|
||||
p.initCancelReader(true) //nolint:errcheck
|
||||
}
|
||||
|
||||
case showCursorMsg:
|
||||
p.renderer.showCursor()
|
||||
|
||||
case hideCursorMsg:
|
||||
p.renderer.hideCursor()
|
||||
|
||||
case enableBracketedPasteMsg:
|
||||
p.renderer.enableBracketedPaste()
|
||||
|
||||
case disableBracketedPasteMsg:
|
||||
p.renderer.disableBracketedPaste()
|
||||
|
||||
case enableReportFocusMsg:
|
||||
p.renderer.enableReportFocus()
|
||||
|
||||
case disableReportFocusMsg:
|
||||
p.renderer.disableReportFocus()
|
||||
|
||||
case execMsg:
|
||||
// NB: this blocks.
|
||||
p.exec(msg.cmd, msg.fn)
|
||||
|
||||
case BatchMsg:
|
||||
for _, cmd := range msg {
|
||||
cmds <- cmd
|
||||
}
|
||||
continue
|
||||
|
||||
case sequenceMsg:
|
||||
go func() {
|
||||
// Execute commands one at a time, in order.
|
||||
for _, cmd := range msg {
|
||||
if cmd == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
msg := cmd()
|
||||
if batchMsg, ok := msg.(BatchMsg); ok {
|
||||
g, _ := errgroup.WithContext(p.ctx)
|
||||
for _, cmd := range batchMsg {
|
||||
cmd := cmd
|
||||
g.Go(func() error {
|
||||
p.Send(cmd())
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
//nolint:errcheck
|
||||
g.Wait() // wait for all commands from batch msg to finish
|
||||
continue
|
||||
}
|
||||
|
||||
p.Send(msg)
|
||||
}
|
||||
}()
|
||||
|
||||
case setWindowTitleMsg:
|
||||
p.SetWindowTitle(string(msg))
|
||||
|
||||
case windowSizeMsg:
|
||||
go p.checkResize()
|
||||
}
|
||||
|
||||
// Process internal messages for the renderer.
|
||||
if r, ok := p.renderer.(*standardRenderer); ok {
|
||||
r.handleMessages(msg)
|
||||
}
|
||||
|
||||
var cmd Cmd
|
||||
model, cmd = model.Update(msg) // run update
|
||||
cmds <- cmd // process command (if any)
|
||||
p.renderer.write(model.View()) // send view to renderer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run initializes the program and runs its event loops, blocking until it gets
|
||||
// terminated by either [Program.Quit], [Program.Kill], or its signal handler.
|
||||
// Returns the final model.
|
||||
func (p *Program) Run() (Model, error) {
|
||||
p.handlers = channelHandlers{}
|
||||
cmds := make(chan Cmd)
|
||||
p.errs = make(chan error)
|
||||
p.finished = make(chan struct{}, 1)
|
||||
|
||||
defer p.cancel()
|
||||
|
||||
switch p.inputType {
|
||||
case defaultInput:
|
||||
p.input = os.Stdin
|
||||
|
||||
// The user has not set a custom input, so we need to check whether or
|
||||
// not standard input is a terminal. If it's not, we open a new TTY for
|
||||
// input. This will allow things to "just work" in cases where data was
|
||||
// piped in or redirected to the application.
|
||||
//
|
||||
// To disable input entirely pass nil to the [WithInput] program option.
|
||||
f, isFile := p.input.(term.File)
|
||||
if !isFile {
|
||||
break
|
||||
}
|
||||
if term.IsTerminal(f.Fd()) {
|
||||
break
|
||||
}
|
||||
|
||||
f, err := openInputTTY()
|
||||
if err != nil {
|
||||
return p.initialModel, err
|
||||
}
|
||||
defer f.Close() //nolint:errcheck
|
||||
p.input = f
|
||||
|
||||
case ttyInput:
|
||||
// Open a new TTY, by request
|
||||
f, err := openInputTTY()
|
||||
if err != nil {
|
||||
return p.initialModel, err
|
||||
}
|
||||
defer f.Close() //nolint:errcheck
|
||||
p.input = f
|
||||
|
||||
case customInput:
|
||||
// (There is nothing extra to do.)
|
||||
}
|
||||
|
||||
// Handle signals.
|
||||
if !p.startupOptions.has(withoutSignalHandler) {
|
||||
p.handlers.add(p.handleSignals())
|
||||
}
|
||||
|
||||
// Recover from panics.
|
||||
if !p.startupOptions.has(withoutCatchPanics) {
|
||||
defer p.recoverFromPanic()
|
||||
}
|
||||
|
||||
// If no renderer is set use the standard one.
|
||||
if p.renderer == nil {
|
||||
p.renderer = newRenderer(p.output, p.startupOptions.has(withANSICompressor), p.fps)
|
||||
}
|
||||
|
||||
// Check if output is a TTY before entering raw mode, hiding the cursor and
|
||||
// so on.
|
||||
if err := p.initTerminal(); err != nil {
|
||||
return p.initialModel, err
|
||||
}
|
||||
|
||||
// Honor program startup options.
|
||||
if p.startupTitle != "" {
|
||||
p.renderer.setWindowTitle(p.startupTitle)
|
||||
}
|
||||
if p.startupOptions&withAltScreen != 0 {
|
||||
p.renderer.enterAltScreen()
|
||||
}
|
||||
if p.startupOptions&withoutBracketedPaste == 0 {
|
||||
p.renderer.enableBracketedPaste()
|
||||
}
|
||||
if p.startupOptions&withMouseCellMotion != 0 {
|
||||
p.renderer.enableMouseCellMotion()
|
||||
p.renderer.enableMouseSGRMode()
|
||||
} else if p.startupOptions&withMouseAllMotion != 0 {
|
||||
p.renderer.enableMouseAllMotion()
|
||||
p.renderer.enableMouseSGRMode()
|
||||
}
|
||||
|
||||
// XXX: Should we enable mouse mode on Windows?
|
||||
// This needs to happen before initializing the cancel and input reader.
|
||||
p.mouseMode = p.startupOptions&withMouseCellMotion != 0 || p.startupOptions&withMouseAllMotion != 0
|
||||
|
||||
if p.startupOptions&withReportFocus != 0 {
|
||||
p.renderer.enableReportFocus()
|
||||
}
|
||||
|
||||
// Start the renderer.
|
||||
p.renderer.start()
|
||||
|
||||
// Initialize the program.
|
||||
model := p.initialModel
|
||||
if initCmd := model.Init(); initCmd != nil {
|
||||
ch := make(chan struct{})
|
||||
p.handlers.add(ch)
|
||||
|
||||
go func() {
|
||||
defer close(ch)
|
||||
|
||||
select {
|
||||
case cmds <- initCmd:
|
||||
case <-p.ctx.Done():
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Render the initial view.
|
||||
p.renderer.write(model.View())
|
||||
|
||||
// Subscribe to user input.
|
||||
if p.input != nil {
|
||||
if err := p.initCancelReader(false); err != nil {
|
||||
return model, err
|
||||
}
|
||||
}
|
||||
|
||||
// Handle resize events.
|
||||
p.handlers.add(p.handleResize())
|
||||
|
||||
// Process commands.
|
||||
p.handlers.add(p.handleCommands(cmds))
|
||||
|
||||
// Run event loop, handle updates and draw.
|
||||
model, err := p.eventLoop(model, cmds)
|
||||
killed := p.ctx.Err() != nil || err != nil
|
||||
if killed && err == nil {
|
||||
err = fmt.Errorf("%w: %s", ErrProgramKilled, p.ctx.Err())
|
||||
}
|
||||
if err == nil {
|
||||
// Ensure we rendered the final state of the model.
|
||||
p.renderer.write(model.View())
|
||||
}
|
||||
|
||||
// Restore terminal state.
|
||||
p.shutdown(killed)
|
||||
|
||||
return model, err
|
||||
}
|
||||
|
||||
// StartReturningModel initializes the program and runs its event loops,
|
||||
// blocking until it gets terminated by either [Program.Quit], [Program.Kill],
|
||||
// or its signal handler. Returns the final model.
|
||||
//
|
||||
// Deprecated: please use [Program.Run] instead.
|
||||
func (p *Program) StartReturningModel() (Model, error) {
|
||||
return p.Run()
|
||||
}
|
||||
|
||||
// Start initializes the program and runs its event loops, blocking until it
|
||||
// gets terminated by either [Program.Quit], [Program.Kill], or its signal
|
||||
// handler.
|
||||
//
|
||||
// Deprecated: please use [Program.Run] instead.
|
||||
func (p *Program) Start() error {
|
||||
_, err := p.Run()
|
||||
return err
|
||||
}
|
||||
|
||||
// Send sends a message to the main update function, effectively allowing
|
||||
// messages to be injected from outside the program for interoperability
|
||||
// purposes.
|
||||
//
|
||||
// If the program hasn't started yet this will be a blocking operation.
|
||||
// If the program has already been terminated this will be a no-op, so it's safe
|
||||
// to send messages after the program has exited.
|
||||
func (p *Program) Send(msg Msg) {
|
||||
select {
|
||||
case <-p.ctx.Done():
|
||||
case p.msgs <- msg:
|
||||
}
|
||||
}
|
||||
|
||||
// Quit is a convenience function for quitting Bubble Tea programs. Use it
|
||||
// when you need to shut down a Bubble Tea program from the outside.
|
||||
//
|
||||
// If you wish to quit from within a Bubble Tea program use the Quit command.
|
||||
//
|
||||
// If the program is not running this will be a no-op, so it's safe to call
|
||||
// if the program is unstarted or has already exited.
|
||||
func (p *Program) Quit() {
|
||||
p.Send(Quit())
|
||||
}
|
||||
|
||||
// Kill stops the program immediately and restores the former terminal state.
|
||||
// The final render that you would normally see when quitting will be skipped.
|
||||
// [program.Run] returns a [ErrProgramKilled] error.
|
||||
func (p *Program) Kill() {
|
||||
p.shutdown(true)
|
||||
}
|
||||
|
||||
// Wait waits/blocks until the underlying Program finished shutting down.
|
||||
func (p *Program) Wait() {
|
||||
<-p.finished
|
||||
}
|
||||
|
||||
// shutdown performs operations to free up resources and restore the terminal
|
||||
// to its original state.
|
||||
func (p *Program) shutdown(kill bool) {
|
||||
p.cancel()
|
||||
|
||||
// Wait for all handlers to finish.
|
||||
p.handlers.shutdown()
|
||||
|
||||
// Check if the cancel reader has been setup before waiting and closing.
|
||||
if p.cancelReader != nil {
|
||||
// Wait for input loop to finish.
|
||||
if p.cancelReader.Cancel() {
|
||||
if !kill {
|
||||
p.waitForReadLoop()
|
||||
}
|
||||
}
|
||||
_ = p.cancelReader.Close()
|
||||
}
|
||||
|
||||
if p.renderer != nil {
|
||||
if kill {
|
||||
p.renderer.kill()
|
||||
} else {
|
||||
p.renderer.stop()
|
||||
}
|
||||
}
|
||||
|
||||
_ = p.restoreTerminalState()
|
||||
if !kill {
|
||||
p.finished <- struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// recoverFromPanic recovers from a panic, prints the stack trace, and restores
|
||||
// the terminal to a usable state.
|
||||
func (p *Program) recoverFromPanic() {
|
||||
if r := recover(); r != nil {
|
||||
p.shutdown(true)
|
||||
fmt.Printf("Caught panic:\n\n%s\n\nRestoring terminal...\n\n", r)
|
||||
debug.PrintStack()
|
||||
}
|
||||
}
|
||||
|
||||
// ReleaseTerminal restores the original terminal state and cancels the input
|
||||
// reader. You can return control to the Program with RestoreTerminal.
|
||||
func (p *Program) ReleaseTerminal() error {
|
||||
atomic.StoreUint32(&p.ignoreSignals, 1)
|
||||
if p.cancelReader != nil {
|
||||
p.cancelReader.Cancel()
|
||||
}
|
||||
|
||||
p.waitForReadLoop()
|
||||
|
||||
if p.renderer != nil {
|
||||
p.renderer.stop()
|
||||
p.altScreenWasActive = p.renderer.altScreen()
|
||||
p.bpWasActive = p.renderer.bracketedPasteActive()
|
||||
p.reportFocus = p.renderer.reportFocus()
|
||||
}
|
||||
|
||||
return p.restoreTerminalState()
|
||||
}
|
||||
|
||||
// RestoreTerminal reinitializes the Program's input reader, restores the
|
||||
// terminal to the former state when the program was running, and repaints.
|
||||
// Use it to reinitialize a Program after running ReleaseTerminal.
|
||||
func (p *Program) RestoreTerminal() error {
|
||||
atomic.StoreUint32(&p.ignoreSignals, 0)
|
||||
|
||||
if err := p.initTerminal(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := p.initCancelReader(false); err != nil {
|
||||
return err
|
||||
}
|
||||
if p.altScreenWasActive {
|
||||
p.renderer.enterAltScreen()
|
||||
} else {
|
||||
// entering alt screen already causes a repaint.
|
||||
go p.Send(repaintMsg{})
|
||||
}
|
||||
if p.renderer != nil {
|
||||
p.renderer.start()
|
||||
}
|
||||
if p.bpWasActive {
|
||||
p.renderer.enableBracketedPaste()
|
||||
}
|
||||
if p.reportFocus {
|
||||
p.renderer.enableReportFocus()
|
||||
}
|
||||
|
||||
// If the output is a terminal, it may have been resized while another
|
||||
// process was at the foreground, in which case we may not have received
|
||||
// SIGWINCH. Detect any size change now and propagate the new size as
|
||||
// needed.
|
||||
go p.checkResize()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Println prints above the Program. This output is unmanaged by the program
|
||||
// and will persist across renders by the Program.
|
||||
//
|
||||
// If the altscreen is active no output will be printed.
|
||||
func (p *Program) Println(args ...interface{}) {
|
||||
p.msgs <- printLineMessage{
|
||||
messageBody: fmt.Sprint(args...),
|
||||
}
|
||||
}
|
||||
|
||||
// Printf prints above the Program. It takes a format template followed by
|
||||
// values similar to fmt.Printf. This output is unmanaged by the program and
|
||||
// will persist across renders by the Program.
|
||||
//
|
||||
// Unlike fmt.Printf (but similar to log.Printf) the message will be print on
|
||||
// its own line.
|
||||
//
|
||||
// If the altscreen is active no output will be printed.
|
||||
func (p *Program) Printf(template string, args ...interface{}) {
|
||||
p.msgs <- printLineMessage{
|
||||
messageBody: fmt.Sprintf(template, args...),
|
||||
}
|
||||
}
|
22
vendor/github.com/charmbracelet/bubbletea/tea_init.go
generated
vendored
Normal file
22
vendor/github.com/charmbracelet/bubbletea/tea_init.go
generated
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
package tea
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// XXX: This is a workaround to make assure that Lip Gloss and Termenv
|
||||
// query the terminal before any Bubble Tea Program runs and acquires the
|
||||
// terminal. Without this, Programs that use Lip Gloss/Termenv might hang
|
||||
// while waiting for a a [termenv.OSCTimeout] while querying the terminal
|
||||
// for its background/foreground colors.
|
||||
//
|
||||
// This happens because Bubble Tea acquires the terminal before termenv
|
||||
// reads any responses.
|
||||
//
|
||||
// Note that this will only affect programs running on the default IO i.e.
|
||||
// [os.Stdout] and [os.Stdin].
|
||||
//
|
||||
// This workaround will be removed in v2.
|
||||
_ = lipgloss.HasDarkBackground()
|
||||
}
|
141
vendor/github.com/charmbracelet/bubbletea/tty.go
generated
vendored
Normal file
141
vendor/github.com/charmbracelet/bubbletea/tty.go
generated
vendored
Normal file
@ -0,0 +1,141 @@
|
||||
package tea
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/x/term"
|
||||
"github.com/muesli/cancelreader"
|
||||
)
|
||||
|
||||
func (p *Program) suspend() {
|
||||
if err := p.ReleaseTerminal(); err != nil {
|
||||
// If we can't release input, abort.
|
||||
return
|
||||
}
|
||||
|
||||
suspendProcess()
|
||||
|
||||
_ = p.RestoreTerminal()
|
||||
go p.Send(ResumeMsg{})
|
||||
}
|
||||
|
||||
func (p *Program) initTerminal() error {
|
||||
if _, ok := p.renderer.(*nilRenderer); ok {
|
||||
// No need to initialize the terminal if we're not rendering
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := p.initInput(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.renderer.hideCursor()
|
||||
return nil
|
||||
}
|
||||
|
||||
// restoreTerminalState restores the terminal to the state prior to running the
|
||||
// Bubble Tea program.
|
||||
func (p *Program) restoreTerminalState() error {
|
||||
if p.renderer != nil {
|
||||
p.renderer.disableBracketedPaste()
|
||||
p.renderer.showCursor()
|
||||
p.disableMouse()
|
||||
|
||||
if p.renderer.reportFocus() {
|
||||
p.renderer.disableReportFocus()
|
||||
}
|
||||
|
||||
if p.renderer.altScreen() {
|
||||
p.renderer.exitAltScreen()
|
||||
|
||||
// give the terminal a moment to catch up
|
||||
time.Sleep(time.Millisecond * 10) //nolint:gomnd
|
||||
}
|
||||
}
|
||||
|
||||
return p.restoreInput()
|
||||
}
|
||||
|
||||
// restoreInput restores the tty input to its original state.
|
||||
func (p *Program) restoreInput() error {
|
||||
if p.ttyInput != nil && p.previousTtyInputState != nil {
|
||||
if err := term.Restore(p.ttyInput.Fd(), p.previousTtyInputState); err != nil {
|
||||
return fmt.Errorf("error restoring console: %w", err)
|
||||
}
|
||||
}
|
||||
if p.ttyOutput != nil && p.previousOutputState != nil {
|
||||
if err := term.Restore(p.ttyOutput.Fd(), p.previousOutputState); err != nil {
|
||||
return fmt.Errorf("error restoring console: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// initCancelReader (re)commences reading inputs.
|
||||
func (p *Program) initCancelReader(cancel bool) error {
|
||||
if cancel && p.cancelReader != nil {
|
||||
p.cancelReader.Cancel()
|
||||
p.waitForReadLoop()
|
||||
}
|
||||
|
||||
var err error
|
||||
p.cancelReader, err = newInputReader(p.input, p.mouseMode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating cancelreader: %w", err)
|
||||
}
|
||||
|
||||
p.readLoopDone = make(chan struct{})
|
||||
go p.readLoop()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Program) readLoop() {
|
||||
defer close(p.readLoopDone)
|
||||
|
||||
err := readInputs(p.ctx, p.msgs, p.cancelReader)
|
||||
if !errors.Is(err, io.EOF) && !errors.Is(err, cancelreader.ErrCanceled) {
|
||||
select {
|
||||
case <-p.ctx.Done():
|
||||
case p.errs <- err:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// waitForReadLoop waits for the cancelReader to finish its read loop.
|
||||
func (p *Program) waitForReadLoop() {
|
||||
select {
|
||||
case <-p.readLoopDone:
|
||||
case <-time.After(500 * time.Millisecond): //nolint:gomnd
|
||||
// The read loop hangs, which means the input
|
||||
// cancelReader's cancel function has returned true even
|
||||
// though it was not able to cancel the read.
|
||||
}
|
||||
}
|
||||
|
||||
// checkResize detects the current size of the output and informs the program
|
||||
// via a WindowSizeMsg.
|
||||
func (p *Program) checkResize() {
|
||||
if p.ttyOutput == nil {
|
||||
// can't query window size
|
||||
return
|
||||
}
|
||||
|
||||
w, h, err := term.GetSize(p.ttyOutput.Fd())
|
||||
if err != nil {
|
||||
select {
|
||||
case <-p.ctx.Done():
|
||||
case p.errs <- err:
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
p.Send(WindowSizeMsg{
|
||||
Width: w,
|
||||
Height: h,
|
||||
})
|
||||
}
|
49
vendor/github.com/charmbracelet/bubbletea/tty_unix.go
generated
vendored
Normal file
49
vendor/github.com/charmbracelet/bubbletea/tty_unix.go
generated
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || aix || zos
|
||||
// +build darwin dragonfly freebsd linux netbsd openbsd solaris aix zos
|
||||
|
||||
package tea
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/charmbracelet/x/term"
|
||||
)
|
||||
|
||||
func (p *Program) initInput() (err error) {
|
||||
// Check if input is a terminal
|
||||
if f, ok := p.input.(term.File); ok && term.IsTerminal(f.Fd()) {
|
||||
p.ttyInput = f
|
||||
p.previousTtyInputState, err = term.MakeRaw(p.ttyInput.Fd())
|
||||
if err != nil {
|
||||
return fmt.Errorf("error entering raw mode: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if f, ok := p.output.(term.File); ok && term.IsTerminal(f.Fd()) {
|
||||
p.ttyOutput = f
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func openInputTTY() (*os.File, error) {
|
||||
f, err := os.Open("/dev/tty")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open a new TTY: %w", err)
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
const suspendSupported = true
|
||||
|
||||
// Send SIGTSTP to the entire process group.
|
||||
func suspendProcess() {
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, syscall.SIGCONT)
|
||||
_ = syscall.Kill(0, syscall.SIGTSTP)
|
||||
// blocks until a CONT happens...
|
||||
<-c
|
||||
}
|
68
vendor/github.com/charmbracelet/bubbletea/tty_windows.go
generated
vendored
Normal file
68
vendor/github.com/charmbracelet/bubbletea/tty_windows.go
generated
vendored
Normal file
@ -0,0 +1,68 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package tea
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/charmbracelet/x/term"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func (p *Program) initInput() (err error) {
|
||||
// Save stdin state and enable VT input
|
||||
// We also need to enable VT
|
||||
// input here.
|
||||
if f, ok := p.input.(term.File); ok && term.IsTerminal(f.Fd()) {
|
||||
p.ttyInput = f
|
||||
p.previousTtyInputState, err = term.MakeRaw(p.ttyInput.Fd())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Enable VT input
|
||||
var mode uint32
|
||||
if err := windows.GetConsoleMode(windows.Handle(p.ttyInput.Fd()), &mode); err != nil {
|
||||
return fmt.Errorf("error getting console mode: %w", err)
|
||||
}
|
||||
|
||||
if err := windows.SetConsoleMode(windows.Handle(p.ttyInput.Fd()), mode|windows.ENABLE_VIRTUAL_TERMINAL_INPUT); err != nil {
|
||||
return fmt.Errorf("error setting console mode: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Save output screen buffer state and enable VT processing.
|
||||
if f, ok := p.output.(term.File); ok && term.IsTerminal(f.Fd()) {
|
||||
p.ttyOutput = f
|
||||
p.previousOutputState, err = term.GetState(f.Fd())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var mode uint32
|
||||
if err := windows.GetConsoleMode(windows.Handle(p.ttyOutput.Fd()), &mode); err != nil {
|
||||
return fmt.Errorf("error getting console mode: %w", err)
|
||||
}
|
||||
|
||||
if err := windows.SetConsoleMode(windows.Handle(p.ttyOutput.Fd()), mode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING); err != nil {
|
||||
return fmt.Errorf("error setting console mode: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Open the Windows equivalent of a TTY.
|
||||
func openInputTTY() (*os.File, error) {
|
||||
f, err := os.OpenFile("CONIN$", os.O_RDWR, 0o644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
const suspendSupported = false
|
||||
|
||||
func suspendProcess() {}
|
15
vendor/github.com/erikgeiser/coninput/.gitignore
generated
vendored
Normal file
15
vendor/github.com/erikgeiser/coninput/.gitignore
generated
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
24
vendor/github.com/erikgeiser/coninput/.golangci.yml
generated
vendored
Normal file
24
vendor/github.com/erikgeiser/coninput/.golangci.yml
generated
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
linters:
|
||||
enable-all: true
|
||||
disable:
|
||||
- golint
|
||||
- interfacer
|
||||
- scopelint
|
||||
- maligned
|
||||
- rowserrcheck
|
||||
- funlen
|
||||
- depguard
|
||||
- goerr113
|
||||
- exhaustivestruct
|
||||
- testpackage
|
||||
- gochecknoglobals
|
||||
- wrapcheck
|
||||
- forbidigo
|
||||
- ifshort
|
||||
- cyclop
|
||||
- gomoddirectives
|
||||
linters-settings:
|
||||
exhaustive:
|
||||
default-signifies-exhaustive: true
|
||||
issues:
|
||||
exclude-use-default: false
|
21
vendor/github.com/erikgeiser/coninput/LICENSE
generated
vendored
Normal file
21
vendor/github.com/erikgeiser/coninput/LICENSE
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Erik G.
|
||||
|
||||
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.
|
2
vendor/github.com/erikgeiser/coninput/README.md
generated
vendored
Normal file
2
vendor/github.com/erikgeiser/coninput/README.md
generated
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# coninput
|
||||
Go library for input handling using Windows Console API
|
205
vendor/github.com/erikgeiser/coninput/keycodes.go
generated
vendored
Normal file
205
vendor/github.com/erikgeiser/coninput/keycodes.go
generated
vendored
Normal file
@ -0,0 +1,205 @@
|
||||
package coninput
|
||||
|
||||
// VirtualKeyCode holds a virtual key code (see
|
||||
// https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes).
|
||||
type VirtualKeyCode uint16
|
||||
|
||||
const (
|
||||
VK_LBUTTON VirtualKeyCode = 0x01
|
||||
VK_RBUTTON VirtualKeyCode = 0x02
|
||||
VK_CANCEL VirtualKeyCode = 0x03
|
||||
VK_MBUTTON VirtualKeyCode = 0x04
|
||||
VK_XBUTTON1 VirtualKeyCode = 0x05
|
||||
VK_XBUTTON2 VirtualKeyCode = 0x06
|
||||
VK_BACK VirtualKeyCode = 0x08
|
||||
VK_TAB VirtualKeyCode = 0x09
|
||||
VK_CLEAR VirtualKeyCode = 0x0C
|
||||
VK_RETURN VirtualKeyCode = 0x0D
|
||||
VK_SHIFT VirtualKeyCode = 0x10
|
||||
VK_CONTROL VirtualKeyCode = 0x11
|
||||
VK_MENU VirtualKeyCode = 0x12
|
||||
VK_PAUSE VirtualKeyCode = 0x13
|
||||
VK_CAPITAL VirtualKeyCode = 0x14
|
||||
VK_KANA VirtualKeyCode = 0x15
|
||||
VK_HANGEUL VirtualKeyCode = 0x15
|
||||
VK_HANGUL VirtualKeyCode = 0x15
|
||||
VK_IME_ON VirtualKeyCode = 0x16
|
||||
VK_JUNJA VirtualKeyCode = 0x17
|
||||
VK_FINAL VirtualKeyCode = 0x18
|
||||
VK_HANJA VirtualKeyCode = 0x19
|
||||
VK_KANJI VirtualKeyCode = 0x19
|
||||
VK_IME_OFF VirtualKeyCode = 0x1A
|
||||
VK_ESCAPE VirtualKeyCode = 0x1B
|
||||
VK_CONVERT VirtualKeyCode = 0x1C
|
||||
VK_NONCONVERT VirtualKeyCode = 0x1D
|
||||
VK_ACCEPT VirtualKeyCode = 0x1E
|
||||
VK_MODECHANGE VirtualKeyCode = 0x1F
|
||||
VK_SPACE VirtualKeyCode = 0x20
|
||||
VK_PRIOR VirtualKeyCode = 0x21
|
||||
VK_NEXT VirtualKeyCode = 0x22
|
||||
VK_END VirtualKeyCode = 0x23
|
||||
VK_HOME VirtualKeyCode = 0x24
|
||||
VK_LEFT VirtualKeyCode = 0x25
|
||||
VK_UP VirtualKeyCode = 0x26
|
||||
VK_RIGHT VirtualKeyCode = 0x27
|
||||
VK_DOWN VirtualKeyCode = 0x28
|
||||
VK_SELECT VirtualKeyCode = 0x29
|
||||
VK_PRINT VirtualKeyCode = 0x2A
|
||||
VK_EXECUTE VirtualKeyCode = 0x2B
|
||||
VK_SNAPSHOT VirtualKeyCode = 0x2C
|
||||
VK_INSERT VirtualKeyCode = 0x2D
|
||||
VK_DELETE VirtualKeyCode = 0x2E
|
||||
VK_HELP VirtualKeyCode = 0x2F
|
||||
VK_0 VirtualKeyCode = 0x30
|
||||
VK_1 VirtualKeyCode = 0x31
|
||||
VK_2 VirtualKeyCode = 0x32
|
||||
VK_3 VirtualKeyCode = 0x33
|
||||
VK_4 VirtualKeyCode = 0x34
|
||||
VK_5 VirtualKeyCode = 0x35
|
||||
VK_6 VirtualKeyCode = 0x36
|
||||
VK_7 VirtualKeyCode = 0x37
|
||||
VK_8 VirtualKeyCode = 0x38
|
||||
VK_9 VirtualKeyCode = 0x39
|
||||
VK_A VirtualKeyCode = 0x41
|
||||
VK_B VirtualKeyCode = 0x42
|
||||
VK_C VirtualKeyCode = 0x43
|
||||
VK_D VirtualKeyCode = 0x44
|
||||
VK_E VirtualKeyCode = 0x45
|
||||
VK_F VirtualKeyCode = 0x46
|
||||
VK_G VirtualKeyCode = 0x47
|
||||
VK_H VirtualKeyCode = 0x48
|
||||
VK_I VirtualKeyCode = 0x49
|
||||
VK_J VirtualKeyCode = 0x4A
|
||||
VK_K VirtualKeyCode = 0x4B
|
||||
VK_L VirtualKeyCode = 0x4C
|
||||
VK_M VirtualKeyCode = 0x4D
|
||||
VK_N VirtualKeyCode = 0x4E
|
||||
VK_O VirtualKeyCode = 0x4F
|
||||
VK_P VirtualKeyCode = 0x50
|
||||
VK_Q VirtualKeyCode = 0x51
|
||||
VK_R VirtualKeyCode = 0x52
|
||||
VK_S VirtualKeyCode = 0x53
|
||||
VK_T VirtualKeyCode = 0x54
|
||||
VK_U VirtualKeyCode = 0x55
|
||||
VK_V VirtualKeyCode = 0x56
|
||||
VK_W VirtualKeyCode = 0x57
|
||||
VK_X VirtualKeyCode = 0x58
|
||||
VK_Y VirtualKeyCode = 0x59
|
||||
VK_Z VirtualKeyCode = 0x5A
|
||||
VK_LWIN VirtualKeyCode = 0x5B
|
||||
VK_RWIN VirtualKeyCode = 0x5C
|
||||
VK_APPS VirtualKeyCode = 0x5D
|
||||
VK_SLEEP VirtualKeyCode = 0x5F
|
||||
VK_NUMPAD0 VirtualKeyCode = 0x60
|
||||
VK_NUMPAD1 VirtualKeyCode = 0x61
|
||||
VK_NUMPAD2 VirtualKeyCode = 0x62
|
||||
VK_NUMPAD3 VirtualKeyCode = 0x63
|
||||
VK_NUMPAD4 VirtualKeyCode = 0x64
|
||||
VK_NUMPAD5 VirtualKeyCode = 0x65
|
||||
VK_NUMPAD6 VirtualKeyCode = 0x66
|
||||
VK_NUMPAD7 VirtualKeyCode = 0x67
|
||||
VK_NUMPAD8 VirtualKeyCode = 0x68
|
||||
VK_NUMPAD9 VirtualKeyCode = 0x69
|
||||
VK_MULTIPLY VirtualKeyCode = 0x6A
|
||||
VK_ADD VirtualKeyCode = 0x6B
|
||||
VK_SEPARATOR VirtualKeyCode = 0x6C
|
||||
VK_SUBTRACT VirtualKeyCode = 0x6D
|
||||
VK_DECIMAL VirtualKeyCode = 0x6E
|
||||
VK_DIVIDE VirtualKeyCode = 0x6F
|
||||
VK_F1 VirtualKeyCode = 0x70
|
||||
VK_F2 VirtualKeyCode = 0x71
|
||||
VK_F3 VirtualKeyCode = 0x72
|
||||
VK_F4 VirtualKeyCode = 0x73
|
||||
VK_F5 VirtualKeyCode = 0x74
|
||||
VK_F6 VirtualKeyCode = 0x75
|
||||
VK_F7 VirtualKeyCode = 0x76
|
||||
VK_F8 VirtualKeyCode = 0x77
|
||||
VK_F9 VirtualKeyCode = 0x78
|
||||
VK_F10 VirtualKeyCode = 0x79
|
||||
VK_F11 VirtualKeyCode = 0x7A
|
||||
VK_F12 VirtualKeyCode = 0x7B
|
||||
VK_F13 VirtualKeyCode = 0x7C
|
||||
VK_F14 VirtualKeyCode = 0x7D
|
||||
VK_F15 VirtualKeyCode = 0x7E
|
||||
VK_F16 VirtualKeyCode = 0x7F
|
||||
VK_F17 VirtualKeyCode = 0x80
|
||||
VK_F18 VirtualKeyCode = 0x81
|
||||
VK_F19 VirtualKeyCode = 0x82
|
||||
VK_F20 VirtualKeyCode = 0x83
|
||||
VK_F21 VirtualKeyCode = 0x84
|
||||
VK_F22 VirtualKeyCode = 0x85
|
||||
VK_F23 VirtualKeyCode = 0x86
|
||||
VK_F24 VirtualKeyCode = 0x87
|
||||
VK_NUMLOCK VirtualKeyCode = 0x90
|
||||
VK_SCROLL VirtualKeyCode = 0x91
|
||||
VK_OEM_NEC_EQUAL VirtualKeyCode = 0x92
|
||||
VK_OEM_FJ_JISHO VirtualKeyCode = 0x92
|
||||
VK_OEM_FJ_MASSHOU VirtualKeyCode = 0x93
|
||||
VK_OEM_FJ_TOUROKU VirtualKeyCode = 0x94
|
||||
VK_OEM_FJ_LOYA VirtualKeyCode = 0x95
|
||||
VK_OEM_FJ_ROYA VirtualKeyCode = 0x96
|
||||
VK_LSHIFT VirtualKeyCode = 0xA0
|
||||
VK_RSHIFT VirtualKeyCode = 0xA1
|
||||
VK_LCONTROL VirtualKeyCode = 0xA2
|
||||
VK_RCONTROL VirtualKeyCode = 0xA3
|
||||
VK_LMENU VirtualKeyCode = 0xA4
|
||||
VK_RMENU VirtualKeyCode = 0xA5
|
||||
VK_BROWSER_BACK VirtualKeyCode = 0xA6
|
||||
VK_BROWSER_FORWARD VirtualKeyCode = 0xA7
|
||||
VK_BROWSER_REFRESH VirtualKeyCode = 0xA8
|
||||
VK_BROWSER_STOP VirtualKeyCode = 0xA9
|
||||
VK_BROWSER_SEARCH VirtualKeyCode = 0xAA
|
||||
VK_BROWSER_FAVORITES VirtualKeyCode = 0xAB
|
||||
VK_BROWSER_HOME VirtualKeyCode = 0xAC
|
||||
VK_VOLUME_MUTE VirtualKeyCode = 0xAD
|
||||
VK_VOLUME_DOWN VirtualKeyCode = 0xAE
|
||||
VK_VOLUME_UP VirtualKeyCode = 0xAF
|
||||
VK_MEDIA_NEXT_TRACK VirtualKeyCode = 0xB0
|
||||
VK_MEDIA_PREV_TRACK VirtualKeyCode = 0xB1
|
||||
VK_MEDIA_STOP VirtualKeyCode = 0xB2
|
||||
VK_MEDIA_PLAY_PAUSE VirtualKeyCode = 0xB3
|
||||
VK_LAUNCH_MAIL VirtualKeyCode = 0xB4
|
||||
VK_LAUNCH_MEDIA_SELECT VirtualKeyCode = 0xB5
|
||||
VK_LAUNCH_APP1 VirtualKeyCode = 0xB6
|
||||
VK_LAUNCH_APP2 VirtualKeyCode = 0xB7
|
||||
VK_OEM_1 VirtualKeyCode = 0xBA
|
||||
VK_OEM_PLUS VirtualKeyCode = 0xBB
|
||||
VK_OEM_COMMA VirtualKeyCode = 0xBC
|
||||
VK_OEM_MINUS VirtualKeyCode = 0xBD
|
||||
VK_OEM_PERIOD VirtualKeyCode = 0xBE
|
||||
VK_OEM_2 VirtualKeyCode = 0xBF
|
||||
VK_OEM_3 VirtualKeyCode = 0xC0
|
||||
VK_OEM_4 VirtualKeyCode = 0xDB
|
||||
VK_OEM_5 VirtualKeyCode = 0xDC
|
||||
VK_OEM_6 VirtualKeyCode = 0xDD
|
||||
VK_OEM_7 VirtualKeyCode = 0xDE
|
||||
VK_OEM_8 VirtualKeyCode = 0xDF
|
||||
VK_OEM_AX VirtualKeyCode = 0xE1
|
||||
VK_OEM_102 VirtualKeyCode = 0xE2
|
||||
VK_ICO_HELP VirtualKeyCode = 0xE3
|
||||
VK_ICO_00 VirtualKeyCode = 0xE4
|
||||
VK_PROCESSKEY VirtualKeyCode = 0xE5
|
||||
VK_ICO_CLEAR VirtualKeyCode = 0xE6
|
||||
VK_OEM_RESET VirtualKeyCode = 0xE9
|
||||
VK_OEM_JUMP VirtualKeyCode = 0xEA
|
||||
VK_OEM_PA1 VirtualKeyCode = 0xEB
|
||||
VK_OEM_PA2 VirtualKeyCode = 0xEC
|
||||
VK_OEM_PA3 VirtualKeyCode = 0xED
|
||||
VK_OEM_WSCTRL VirtualKeyCode = 0xEE
|
||||
VK_OEM_CUSEL VirtualKeyCode = 0xEF
|
||||
VK_OEM_ATTN VirtualKeyCode = 0xF0
|
||||
VK_OEM_FINISH VirtualKeyCode = 0xF1
|
||||
VK_OEM_COPY VirtualKeyCode = 0xF2
|
||||
VK_OEM_AUTO VirtualKeyCode = 0xF3
|
||||
VK_OEM_ENLW VirtualKeyCode = 0xF4
|
||||
VK_OEM_BACKTAB VirtualKeyCode = 0xF5
|
||||
VK_ATTN VirtualKeyCode = 0xF6
|
||||
VK_CRSEL VirtualKeyCode = 0xF7
|
||||
VK_EXSEL VirtualKeyCode = 0xF8
|
||||
VK_EREOF VirtualKeyCode = 0xF9
|
||||
VK_PLAY VirtualKeyCode = 0xFA
|
||||
VK_ZOOM VirtualKeyCode = 0xFB
|
||||
VK_NONAME VirtualKeyCode = 0xFC
|
||||
VK_PA1 VirtualKeyCode = 0xFD
|
||||
VK_OEM_CLEAR VirtualKeyCode = 0xFE
|
||||
)
|
82
vendor/github.com/erikgeiser/coninput/mode.go
generated
vendored
Normal file
82
vendor/github.com/erikgeiser/coninput/mode.go
generated
vendored
Normal file
@ -0,0 +1,82 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package coninput
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
// AddInputModes returns the given mode with one or more additional modes enabled.
|
||||
func AddInputModes(mode uint32, enableModes ...uint32) uint32 {
|
||||
for _, enableMode := range enableModes {
|
||||
mode |= enableMode
|
||||
}
|
||||
|
||||
return mode
|
||||
}
|
||||
|
||||
// RemoveInputModes returns the given mode with one or more additional modes disabled.
|
||||
func RemoveInputModes(mode uint32, disableModes ...uint32) uint32 {
|
||||
for _, disableMode := range disableModes {
|
||||
mode &^= disableMode
|
||||
}
|
||||
|
||||
return mode
|
||||
}
|
||||
|
||||
// ToggleInputModes returns the given mode with one or more additional modes toggeled.
|
||||
func ToggleInputModes(mode uint32, toggleModes ...uint32) uint32 {
|
||||
for _, toggeMode := range toggleModes {
|
||||
mode ^= toggeMode
|
||||
}
|
||||
|
||||
return mode
|
||||
}
|
||||
|
||||
var inputModes = []struct {
|
||||
mode uint32
|
||||
name string
|
||||
}{
|
||||
{mode: windows.ENABLE_ECHO_INPUT, name: "ENABLE_ECHO_INPUT"},
|
||||
{mode: windows.ENABLE_INSERT_MODE, name: "ENABLE_INSERT_MODE"},
|
||||
{mode: windows.ENABLE_LINE_INPUT, name: "ENABLE_LINE_INPUT"},
|
||||
{mode: windows.ENABLE_MOUSE_INPUT, name: "ENABLE_MOUSE_INPUT"},
|
||||
{mode: windows.ENABLE_PROCESSED_INPUT, name: "ENABLE_PROCESSED_INPUT"},
|
||||
{mode: windows.ENABLE_QUICK_EDIT_MODE, name: "ENABLE_QUICK_EDIT_MODE"},
|
||||
{mode: windows.ENABLE_WINDOW_INPUT, name: "ENABLE_WINDOW_INPUT"},
|
||||
{mode: windows.ENABLE_VIRTUAL_TERMINAL_INPUT, name: "ENABLE_VIRTUAL_TERMINAL_INPUT"},
|
||||
}
|
||||
|
||||
// ListInputMode returnes the isolated enabled input modes as a list.
|
||||
func ListInputModes(mode uint32) []uint32 {
|
||||
modes := []uint32{}
|
||||
|
||||
for _, inputMode := range inputModes {
|
||||
if mode&inputMode.mode > 0 {
|
||||
modes = append(modes, inputMode.mode)
|
||||
}
|
||||
}
|
||||
|
||||
return modes
|
||||
}
|
||||
|
||||
// ListInputMode returnes the isolated enabled input mode names as a list.
|
||||
func ListInputModeNames(mode uint32) []string {
|
||||
modes := []string{}
|
||||
|
||||
for _, inputMode := range inputModes {
|
||||
if mode&inputMode.mode > 0 {
|
||||
modes = append(modes, inputMode.name)
|
||||
}
|
||||
}
|
||||
|
||||
return modes
|
||||
}
|
||||
|
||||
// DescribeInputMode returns a string containing the names of each enabled input mode.
|
||||
func DescribeInputMode(mode uint32) string {
|
||||
return strings.Join(ListInputModeNames(mode), "|")
|
||||
}
|
154
vendor/github.com/erikgeiser/coninput/read.go
generated
vendored
Normal file
154
vendor/github.com/erikgeiser/coninput/read.go
generated
vendored
Normal file
@ -0,0 +1,154 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package coninput
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var (
|
||||
modkernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||
procReadConsoleInputW = modkernel32.NewProc("ReadConsoleInputW")
|
||||
procPeekConsoleInputW = modkernel32.NewProc("PeekConsoleInputW")
|
||||
procGetNumberOfConsoleInputEvents = modkernel32.NewProc("GetNumberOfConsoleInputEvents")
|
||||
procFlushConsoleInputBuffer = modkernel32.NewProc("FlushConsoleInputBuffer")
|
||||
)
|
||||
|
||||
// NewStdinHandle is a shortcut for windows.GetStdHandle(windows.STD_INPUT_HANDLE).
|
||||
func NewStdinHandle() (windows.Handle, error) {
|
||||
return windows.GetStdHandle(windows.STD_INPUT_HANDLE)
|
||||
}
|
||||
|
||||
// WinReadConsoleInput is a thin wrapper around the Windows console API function
|
||||
// ReadConsoleInput (see
|
||||
// https://docs.microsoft.com/en-us/windows/console/readconsoleinput). In most
|
||||
// cases it is more practical to either use ReadConsoleInput or
|
||||
// ReadNConsoleInputs.
|
||||
func WinReadConsoleInput(consoleInput windows.Handle, buffer *InputRecord,
|
||||
length uint32, numberOfEventsRead *uint32) error {
|
||||
r, _, e := syscall.Syscall6(procReadConsoleInputW.Addr(), 4,
|
||||
uintptr(consoleInput), uintptr(unsafe.Pointer(buffer)), uintptr(length),
|
||||
uintptr(unsafe.Pointer(numberOfEventsRead)), 0, 0)
|
||||
if r == 0 {
|
||||
return error(e)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadNConsoleInputs is a wrapper around ReadConsoleInput (see
|
||||
// https://docs.microsoft.com/en-us/windows/console/readconsoleinput) that
|
||||
// automates the event buffer allocation in oder to provide io.Reader-like
|
||||
// sematics. maxEvents must be greater than zero.
|
||||
func ReadNConsoleInputs(console windows.Handle, maxEvents uint32) ([]InputRecord, error) {
|
||||
if maxEvents == 0 {
|
||||
return nil, fmt.Errorf("maxEvents cannot be zero")
|
||||
}
|
||||
|
||||
var inputRecords = make([]InputRecord, maxEvents)
|
||||
n, err := ReadConsoleInput(console, inputRecords)
|
||||
|
||||
return inputRecords[:n], err
|
||||
}
|
||||
|
||||
// ReadConsoleInput provides an ideomatic interface to the Windows console API
|
||||
// function ReadConsoleInput (see
|
||||
// https://docs.microsoft.com/en-us/windows/console/readconsoleinput). The size
|
||||
// of inputRecords must be greater than zero.
|
||||
func ReadConsoleInput(console windows.Handle, inputRecords []InputRecord) (uint32, error) {
|
||||
if len(inputRecords) == 0 {
|
||||
return 0, fmt.Errorf("size of input record buffer cannot be zero")
|
||||
}
|
||||
|
||||
var read uint32
|
||||
err := WinReadConsoleInput(console, &inputRecords[0], uint32(len(inputRecords)), &read)
|
||||
|
||||
return read, err
|
||||
}
|
||||
|
||||
// WinPeekConsoleInput is a thin wrapper around the Windows console API function
|
||||
// PeekConsoleInput (see
|
||||
// https://docs.microsoft.com/en-us/windows/console/peekconsoleinput). In most
|
||||
// cases it is more practical to either use PeekConsoleInput or
|
||||
// PeekNConsoleInputs.
|
||||
func WinPeekConsoleInput(consoleInput windows.Handle, buffer *InputRecord,
|
||||
length uint32, numberOfEventsRead *uint32) error {
|
||||
r, _, e := syscall.Syscall6(procPeekConsoleInputW.Addr(), 4,
|
||||
uintptr(consoleInput), uintptr(unsafe.Pointer(buffer)), uintptr(length),
|
||||
uintptr(unsafe.Pointer(numberOfEventsRead)), 0, 0)
|
||||
if r == 0 {
|
||||
return error(e)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// PeekNConsoleInputs is a wrapper around PeekConsoleInput (see
|
||||
// https://docs.microsoft.com/en-us/windows/console/peekconsoleinput) that
|
||||
// automates the event buffer allocation in oder to provide io.Reader-like
|
||||
// sematics. maxEvents must be greater than zero.
|
||||
func PeekNConsoleInputs(console windows.Handle, maxEvents uint32) ([]InputRecord, error) {
|
||||
if maxEvents == 0 {
|
||||
return nil, fmt.Errorf("maxEvents cannot be zero")
|
||||
}
|
||||
|
||||
var inputRecords = make([]InputRecord, maxEvents)
|
||||
n, err := PeekConsoleInput(console, inputRecords)
|
||||
|
||||
return inputRecords[:n], err
|
||||
}
|
||||
|
||||
// PeekConsoleInput provides an ideomatic interface to the Windows console API
|
||||
// function PeekConsoleInput (see
|
||||
// https://docs.microsoft.com/en-us/windows/console/peekconsoleinput). The size
|
||||
// of inputRecords must be greater than zero.
|
||||
func PeekConsoleInput(console windows.Handle, inputRecords []InputRecord) (uint32, error) {
|
||||
if len(inputRecords) == 0 {
|
||||
return 0, fmt.Errorf("size of input record buffer cannot be zero")
|
||||
}
|
||||
|
||||
var read uint32
|
||||
|
||||
err := WinPeekConsoleInput(console, &inputRecords[0], uint32(len(inputRecords)), &read)
|
||||
|
||||
return read, err
|
||||
}
|
||||
|
||||
// WinGetNumberOfConsoleInputEvents provides an ideomatic interface to the
|
||||
// Windows console API function GetNumberOfConsoleInputEvents (see
|
||||
// https://docs.microsoft.com/en-us/windows/console/getnumberofconsoleinputevents).
|
||||
func WinGetNumberOfConsoleInputEvents(consoleInput windows.Handle, numberOfEvents *uint32) error {
|
||||
r, _, e := syscall.Syscall6(procGetNumberOfConsoleInputEvents.Addr(), 2,
|
||||
uintptr(consoleInput), uintptr(unsafe.Pointer(numberOfEvents)), 0,
|
||||
0, 0, 0)
|
||||
if r == 0 {
|
||||
return error(e)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetNumberOfConsoleInputEvents provides an ideomatic interface to the Windows
|
||||
// console API function GetNumberOfConsoleInputEvents (see
|
||||
// https://docs.microsoft.com/en-us/windows/console/getnumberofconsoleinputevents).
|
||||
func GetNumberOfConsoleInputEvents(console windows.Handle) (uint32, error) {
|
||||
var nEvents uint32
|
||||
err := WinGetNumberOfConsoleInputEvents(console, &nEvents)
|
||||
|
||||
return nEvents, err
|
||||
}
|
||||
|
||||
func FlushConsoleInputBuffer(consoleInput windows.Handle) error {
|
||||
r, _, e := syscall.Syscall(procFlushConsoleInputBuffer.Addr(), 1, uintptr(consoleInput), 0, 0)
|
||||
if r == 0 {
|
||||
return error(e)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
486
vendor/github.com/erikgeiser/coninput/records.go
generated
vendored
Normal file
486
vendor/github.com/erikgeiser/coninput/records.go
generated
vendored
Normal file
@ -0,0 +1,486 @@
|
||||
package coninput
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
maxEventSize = 16
|
||||
wordPaddingBytes = 2
|
||||
)
|
||||
|
||||
// EventType denots the type of an event
|
||||
type EventType uint16
|
||||
|
||||
// EventUnion is the union data type that contains the data for any event.
|
||||
type EventUnion [maxEventSize]byte
|
||||
|
||||
// InputRecord corresponds to the INPUT_RECORD structure from the Windows
|
||||
// console API (see
|
||||
// https://docs.microsoft.com/en-us/windows/console/input-record-str).
|
||||
type InputRecord struct {
|
||||
// EventType specifies the type of event that helt in Event.
|
||||
EventType EventType
|
||||
|
||||
// Padding of the 16-bit EventType to a whole 32-bit dword.
|
||||
_ [wordPaddingBytes]byte
|
||||
|
||||
// Event holds the actual event data. Use Unrap to access it as its
|
||||
// respective event type.
|
||||
Event EventUnion
|
||||
}
|
||||
|
||||
// String implements fmt.Stringer for InputRecord.
|
||||
func (ir InputRecord) String() string {
|
||||
return ir.Unwrap().String()
|
||||
}
|
||||
|
||||
// Unwrap parses the event data into an EventRecord of the respective event
|
||||
// type. The data in the returned EventRecord does not contain any references to
|
||||
// the passed InputRecord.
|
||||
func (ir InputRecord) Unwrap() EventRecord {
|
||||
switch ir.EventType {
|
||||
case FocusEventType:
|
||||
return FocusEventRecord{SetFocus: ir.Event[0] > 0}
|
||||
case KeyEventType:
|
||||
return KeyEventRecord{
|
||||
KeyDown: binary.LittleEndian.Uint32(ir.Event[0:4]) > 0,
|
||||
RepeatCount: binary.LittleEndian.Uint16(ir.Event[4:6]),
|
||||
VirtualKeyCode: VirtualKeyCode(binary.LittleEndian.Uint16(ir.Event[6:8])),
|
||||
VirtualScanCode: VirtualKeyCode(binary.LittleEndian.Uint16(ir.Event[8:10])),
|
||||
Char: rune(binary.LittleEndian.Uint16(ir.Event[10:12])),
|
||||
ControlKeyState: ControlKeyState(binary.LittleEndian.Uint32(ir.Event[12:16])),
|
||||
}
|
||||
case MouseEventType:
|
||||
m := MouseEventRecord{
|
||||
MousePositon: Coord{
|
||||
X: binary.LittleEndian.Uint16(ir.Event[0:2]),
|
||||
Y: binary.LittleEndian.Uint16(ir.Event[2:4]),
|
||||
},
|
||||
ButtonState: ButtonState(binary.LittleEndian.Uint32(ir.Event[4:8])),
|
||||
ControlKeyState: ControlKeyState(binary.LittleEndian.Uint32(ir.Event[8:12])),
|
||||
EventFlags: EventFlags(binary.LittleEndian.Uint32(ir.Event[12:16])),
|
||||
}
|
||||
|
||||
if (m.EventFlags&MOUSE_WHEELED > 0) || (m.EventFlags&MOUSE_HWHEELED > 0) {
|
||||
if int16(highWord(uint32(m.ButtonState))) > 0 {
|
||||
m.WheelDirection = 1
|
||||
} else {
|
||||
m.WheelDirection = -1
|
||||
}
|
||||
}
|
||||
|
||||
return m
|
||||
case WindowBufferSizeEventType:
|
||||
return WindowBufferSizeEventRecord{
|
||||
Size: Coord{
|
||||
X: binary.LittleEndian.Uint16(ir.Event[0:2]),
|
||||
Y: binary.LittleEndian.Uint16(ir.Event[2:4]),
|
||||
},
|
||||
}
|
||||
case MenuEventType:
|
||||
return MenuEventRecord{
|
||||
CommandID: binary.LittleEndian.Uint32(ir.Event[0:4]),
|
||||
}
|
||||
default:
|
||||
return &UnknownEvent{InputRecord: ir}
|
||||
}
|
||||
}
|
||||
|
||||
// EventRecord represents one of the following event types:
|
||||
// TypeFocusEventRecord, TypeKeyEventRecord, TypeMouseEventRecord,
|
||||
// TypeWindowBufferSizeEvent, TypeMenuEventRecord and UnknownEvent.
|
||||
type EventRecord interface {
|
||||
Type() string
|
||||
fmt.Stringer
|
||||
}
|
||||
|
||||
// FocusEventType is the event type for a FocusEventRecord (see
|
||||
// https://docs.microsoft.com/en-us/windows/console/input-record-str).
|
||||
const FocusEventType EventType = 0x0010
|
||||
|
||||
// FocusEventRecord represent the FOCUS_EVENT_RECORD structure from the Windows
|
||||
// console API (see
|
||||
// https://docs.microsoft.com/en-us/windows/console/focus-event-record-str).
|
||||
// These events are used internally by the Windows console API and should be
|
||||
// ignored.
|
||||
type FocusEventRecord struct {
|
||||
// SetFocus is reserved and should not be used.
|
||||
SetFocus bool
|
||||
}
|
||||
|
||||
// Ensure that FocusEventRecord satisfies EventRecord interface.
|
||||
var _ EventRecord = FocusEventRecord{}
|
||||
|
||||
// Type ensures that FocusEventRecord satisfies EventRecord interface.
|
||||
func (e FocusEventRecord) Type() string { return "FocusEvent" }
|
||||
|
||||
// String ensures that FocusEventRecord satisfies EventRecord and fmt.Stringer
|
||||
// interfaces.
|
||||
func (e FocusEventRecord) String() string { return fmt.Sprintf("%s[%v]", e.Type(), e.SetFocus) }
|
||||
|
||||
// KeyEventType is the event type for a KeyEventRecord (see
|
||||
// https://docs.microsoft.com/en-us/windows/console/input-record-str).
|
||||
const KeyEventType EventType = 0x0001
|
||||
|
||||
// KeyEventRecord represent the KEY_EVENT_RECORD structure from the Windows
|
||||
// console API (see
|
||||
// https://docs.microsoft.com/en-us/windows/console/key-event-record-str).
|
||||
type KeyEventRecord struct {
|
||||
// KeyDown specified whether the key is pressed or released.
|
||||
KeyDown bool
|
||||
|
||||
// RepeatCount indicates that a key is being held down. For example, when a
|
||||
// key is held down, five events with RepeatCount equal to 1 may be
|
||||
// generated, one event with RepeatCount equal to 5, or multiple events
|
||||
// with RepeatCount greater than or equal to 1.
|
||||
RepeatCount uint16
|
||||
|
||||
// VirtualKeyCode identifies the given key in a device-independent manner
|
||||
// (see
|
||||
// https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes).
|
||||
VirtualKeyCode VirtualKeyCode
|
||||
|
||||
// VirtualScanCode represents the device-dependent value generated by the
|
||||
// keyboard hardware.
|
||||
VirtualScanCode VirtualKeyCode
|
||||
|
||||
// Char is the character that corresponds to the pressed key. Char can be
|
||||
// zero for some keys.
|
||||
Char rune
|
||||
|
||||
//ControlKeyState holds the state of the control keys.
|
||||
ControlKeyState ControlKeyState
|
||||
}
|
||||
|
||||
// Ensure that KeyEventRecord satisfies EventRecord interface.
|
||||
var _ EventRecord = KeyEventRecord{}
|
||||
|
||||
// Type ensures that KeyEventRecord satisfies EventRecord interface.
|
||||
func (e KeyEventRecord) Type() string { return "KeyEvent" }
|
||||
|
||||
// String ensures that KeyEventRecord satisfies EventRecord and fmt.Stringer
|
||||
// interfaces.
|
||||
func (e KeyEventRecord) String() string {
|
||||
infos := []string{}
|
||||
|
||||
repeat := ""
|
||||
if e.RepeatCount > 1 {
|
||||
repeat = "x" + strconv.Itoa(int(e.RepeatCount))
|
||||
}
|
||||
|
||||
infos = append(infos, fmt.Sprintf("%q%s", e.Char, repeat))
|
||||
|
||||
direction := "up"
|
||||
if e.KeyDown {
|
||||
direction = "down"
|
||||
}
|
||||
|
||||
infos = append(infos, direction)
|
||||
|
||||
if e.ControlKeyState != NO_CONTROL_KEY {
|
||||
infos = append(infos, e.ControlKeyState.String())
|
||||
}
|
||||
|
||||
infos = append(infos, fmt.Sprintf("KeyCode: %d", e.VirtualKeyCode))
|
||||
infos = append(infos, fmt.Sprintf("ScanCode: %d", e.VirtualScanCode))
|
||||
|
||||
return fmt.Sprintf("%s[%s]", e.Type(), strings.Join(infos, ", "))
|
||||
}
|
||||
|
||||
// MenuEventType is the event type for a MenuEventRecord (see
|
||||
// https://docs.microsoft.com/en-us/windows/console/input-record-str).
|
||||
const MenuEventType EventType = 0x0008
|
||||
|
||||
// MenuEventRecord represent the MENU_EVENT_RECORD structure from the Windows
|
||||
// console API (see
|
||||
// https://docs.microsoft.com/en-us/windows/console/menu-event-record-str).
|
||||
// These events are deprecated by the Windows console API and should be ignored.
|
||||
type MenuEventRecord struct {
|
||||
CommandID uint32
|
||||
}
|
||||
|
||||
// Ensure that MenuEventRecord satisfies EventRecord interface.
|
||||
var _ EventRecord = MenuEventRecord{}
|
||||
|
||||
// Type ensures that MenuEventRecord satisfies EventRecord interface.
|
||||
func (e MenuEventRecord) Type() string { return "MenuEvent" }
|
||||
|
||||
// String ensures that MenuEventRecord satisfies EventRecord and fmt.Stringer
|
||||
// interfaces.
|
||||
func (e MenuEventRecord) String() string { return fmt.Sprintf("MenuEvent[%d]", e.CommandID) }
|
||||
|
||||
// MouseEventType is the event type for a MouseEventRecord (see
|
||||
// https://docs.microsoft.com/en-us/windows/console/input-record-str).
|
||||
const MouseEventType EventType = 0x0002
|
||||
|
||||
// MouseEventRecord represent the MOUSE_EVENT_RECORD structure from the Windows
|
||||
// console API (see
|
||||
// https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str).
|
||||
type MouseEventRecord struct {
|
||||
// MousePosition contains the location of the cursor, in terms of the
|
||||
// console screen buffer's character-cell coordinates.
|
||||
MousePositon Coord
|
||||
|
||||
// ButtonState holds the status of the mouse buttons.
|
||||
ButtonState ButtonState
|
||||
|
||||
// ControlKeyState holds the state of the control keys.
|
||||
ControlKeyState ControlKeyState
|
||||
|
||||
// EventFlags specify tge type of mouse event.
|
||||
EventFlags EventFlags
|
||||
|
||||
// WheelDirection specified the direction in which the mouse wheel is
|
||||
// spinning when EventFlags contains MOUSE_HWHEELED or MOUSE_WHEELED. When
|
||||
// the event flags specify MOUSE_WHEELED it is 1 if the wheel rotated
|
||||
// forward (away from the user) or -1 when it rotates backwards. When
|
||||
// MOUSE_HWHEELED is specified it is 1 when the wheel rotates right and -1
|
||||
// when it rotates left. When the EventFlags do not indicate a mouse wheel
|
||||
// event it is 0.
|
||||
WheelDirection int
|
||||
}
|
||||
|
||||
// Ensure that MouseEventRecord satisfies EventRecord interface.
|
||||
var _ EventRecord = MouseEventRecord{}
|
||||
|
||||
func (e MouseEventRecord) WheelDirectionName() string {
|
||||
if e.EventFlags&MOUSE_WHEELED > 0 {
|
||||
if e.WheelDirection > 0 {
|
||||
return "Forward"
|
||||
}
|
||||
|
||||
return "Backward"
|
||||
} else if e.EventFlags&MOUSE_HWHEELED > 0 {
|
||||
if e.WheelDirection > 0 {
|
||||
return "Right"
|
||||
}
|
||||
|
||||
return "Left"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// Type ensures that MouseEventRecord satisfies EventRecord interface.
|
||||
func (e MouseEventRecord) Type() string { return "MouseEvent" }
|
||||
|
||||
// String ensures that MouseEventRecord satisfies EventRecord and fmt.Stringer
|
||||
// interfaces.
|
||||
func (e MouseEventRecord) String() string {
|
||||
infos := []string{e.MousePositon.String()}
|
||||
|
||||
if e.ButtonState&0xFF != 0 {
|
||||
infos = append(infos, e.ButtonState.String())
|
||||
}
|
||||
|
||||
eventDescription := e.EventFlags.String()
|
||||
|
||||
wheelDirection := e.WheelDirectionName()
|
||||
if wheelDirection != "" {
|
||||
eventDescription += "(" + wheelDirection + ")"
|
||||
}
|
||||
|
||||
infos = append(infos, eventDescription)
|
||||
|
||||
if e.ControlKeyState != NO_CONTROL_KEY {
|
||||
infos = append(infos, e.ControlKeyState.String())
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s[%s]", e.Type(), strings.Join(infos, ", "))
|
||||
}
|
||||
|
||||
// WindowBufferSizeEventType is the event type for a WindowBufferSizeEventRecord
|
||||
// (see https://docs.microsoft.com/en-us/windows/console/input-record-str).
|
||||
const WindowBufferSizeEventType EventType = 0x0004
|
||||
|
||||
// WindowBufferSizeEventRecord represent the WINDOW_BUFFER_SIZE_RECORD structure
|
||||
// from the Windows console API (see
|
||||
// https://docs.microsoft.com/en-us/windows/console/window-buffer-size-record-str).
|
||||
type WindowBufferSizeEventRecord struct {
|
||||
// Size contains the size of the console screen buffer, in character cell columns and rows.
|
||||
Size Coord
|
||||
}
|
||||
|
||||
// Ensure that WindowBufferSizeEventRecord satisfies EventRecord interface.
|
||||
var _ EventRecord = WindowBufferSizeEventRecord{}
|
||||
|
||||
// Type ensures that WindowBufferSizeEventRecord satisfies EventRecord interface.
|
||||
func (e WindowBufferSizeEventRecord) Type() string { return "WindowBufferSizeEvent" }
|
||||
|
||||
// String ensures that WindowBufferSizeEventRecord satisfies EventRecord and fmt.Stringer
|
||||
// interfaces.
|
||||
func (e WindowBufferSizeEventRecord) String() string {
|
||||
return fmt.Sprintf("WindowBufferSizeEvent[%s]", e.Size)
|
||||
}
|
||||
|
||||
// UnknownEvent is generated when the event type does not match one of the
|
||||
// following types: TypeFocusEventRecord, TypeKeyEventRecord,
|
||||
// TypeMouseEventRecord, TypeWindowBufferSizeEvent, TypeMenuEventRecord and
|
||||
// UnknownEvent.
|
||||
type UnknownEvent struct {
|
||||
InputRecord
|
||||
}
|
||||
|
||||
// Ensure that UnknownEvent satisfies EventRecord interface.
|
||||
var _ EventRecord = UnknownEvent{}
|
||||
|
||||
// Type ensures that UnknownEvent satisfies EventRecord interface.
|
||||
func (e UnknownEvent) Type() string { return "UnknownEvent" }
|
||||
|
||||
// String ensures that UnknownEvent satisfies EventRecord and fmt.Stringer
|
||||
// interfaces.
|
||||
func (e UnknownEvent) String() string {
|
||||
return fmt.Sprintf("%s[Type: %d, Data: %v]", e.Type(), e.InputRecord.EventType, e.InputRecord.Event[:])
|
||||
}
|
||||
|
||||
// Coord represent the COORD structure from the Windows
|
||||
// console API (see https://docs.microsoft.com/en-us/windows/console/coord-str).
|
||||
type Coord struct {
|
||||
// X is the horizontal coordinate or column value. The units depend on the function call.
|
||||
X uint16
|
||||
// Y is the vertical coordinate or row value. The units depend on the function call.
|
||||
Y uint16
|
||||
}
|
||||
|
||||
// String ensures that Coord satisfies the fmt.Stringer interface.
|
||||
func (c Coord) String() string {
|
||||
return fmt.Sprintf("(%d, %d)", c.X, c.Y)
|
||||
}
|
||||
|
||||
// ButtonState holds the state of the mouse buttons (see
|
||||
// https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str).
|
||||
type ButtonState uint32
|
||||
|
||||
func (bs ButtonState) Contains(state ButtonState) bool {
|
||||
return bs&state > 0
|
||||
}
|
||||
|
||||
// String ensures that ButtonState satisfies the fmt.Stringer interface.
|
||||
func (bs ButtonState) String() string {
|
||||
switch {
|
||||
case bs&FROM_LEFT_1ST_BUTTON_PRESSED > 0:
|
||||
return "Left"
|
||||
case bs&FROM_LEFT_2ND_BUTTON_PRESSED > 0:
|
||||
return "2"
|
||||
case bs&FROM_LEFT_3RD_BUTTON_PRESSED > 0:
|
||||
return "3"
|
||||
case bs&FROM_LEFT_4TH_BUTTON_PRESSED > 0:
|
||||
return "4"
|
||||
case bs&RIGHTMOST_BUTTON_PRESSED > 0:
|
||||
return "Right"
|
||||
case bs&0xFF == 0:
|
||||
return "No Button"
|
||||
default:
|
||||
return fmt.Sprintf("Unknown(%d)", bs)
|
||||
}
|
||||
}
|
||||
|
||||
func (bs ButtonState) IsReleased() bool {
|
||||
return bs&0xff > 0
|
||||
}
|
||||
|
||||
// Valid values for ButtonState.
|
||||
const (
|
||||
FROM_LEFT_1ST_BUTTON_PRESSED ButtonState = 0x0001
|
||||
RIGHTMOST_BUTTON_PRESSED ButtonState = 0x0002
|
||||
FROM_LEFT_2ND_BUTTON_PRESSED ButtonState = 0x0004
|
||||
FROM_LEFT_3RD_BUTTON_PRESSED ButtonState = 0x0008
|
||||
FROM_LEFT_4TH_BUTTON_PRESSED ButtonState = 0x0010
|
||||
)
|
||||
|
||||
// ControlKeyState holds the state of the control keys for key and mouse events
|
||||
// (see https://docs.microsoft.com/en-us/windows/console/key-event-record-str
|
||||
// and https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str).
|
||||
type ControlKeyState uint32
|
||||
|
||||
func (cks ControlKeyState) Contains(state ControlKeyState) bool {
|
||||
return cks&state > 0
|
||||
}
|
||||
|
||||
// Valid values for ControlKeyState.
|
||||
const (
|
||||
CAPSLOCK_ON ControlKeyState = 0x0080
|
||||
ENHANCED_KEY ControlKeyState = 0x0100
|
||||
LEFT_ALT_PRESSED ControlKeyState = 0x0002
|
||||
LEFT_CTRL_PRESSED ControlKeyState = 0x0008
|
||||
NUMLOCK_ON ControlKeyState = 0x0020
|
||||
RIGHT_ALT_PRESSED ControlKeyState = 0x0001
|
||||
RIGHT_CTRL_PRESSED ControlKeyState = 0x0004
|
||||
SCROLLLOCK_ON ControlKeyState = 0x0040
|
||||
SHIFT_PRESSED ControlKeyState = 0x0010
|
||||
NO_CONTROL_KEY ControlKeyState = 0x0000
|
||||
)
|
||||
|
||||
// String ensures that ControlKeyState satisfies the fmt.Stringer interface.
|
||||
func (cks ControlKeyState) String() string {
|
||||
controlKeys := []string{}
|
||||
|
||||
switch {
|
||||
case cks&CAPSLOCK_ON > 0:
|
||||
controlKeys = append(controlKeys, "CapsLock")
|
||||
case cks&ENHANCED_KEY > 0:
|
||||
controlKeys = append(controlKeys, "Enhanced")
|
||||
case cks&LEFT_ALT_PRESSED > 0:
|
||||
controlKeys = append(controlKeys, "Alt")
|
||||
case cks&LEFT_CTRL_PRESSED > 0:
|
||||
controlKeys = append(controlKeys, "CTRL")
|
||||
case cks&NUMLOCK_ON > 0:
|
||||
controlKeys = append(controlKeys, "NumLock")
|
||||
case cks&RIGHT_ALT_PRESSED > 0:
|
||||
controlKeys = append(controlKeys, "RightAlt")
|
||||
case cks&RIGHT_CTRL_PRESSED > 0:
|
||||
controlKeys = append(controlKeys, "RightCTRL")
|
||||
case cks&SCROLLLOCK_ON > 0:
|
||||
controlKeys = append(controlKeys, "ScrollLock")
|
||||
case cks&SHIFT_PRESSED > 0:
|
||||
controlKeys = append(controlKeys, "Shift")
|
||||
case cks == NO_CONTROL_KEY:
|
||||
default:
|
||||
return fmt.Sprintf("Unknown(%d)", cks)
|
||||
}
|
||||
|
||||
return strings.Join(controlKeys, ",")
|
||||
}
|
||||
|
||||
// EventFlags specifies the type of a mouse event (see
|
||||
// https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str).
|
||||
type EventFlags uint32
|
||||
|
||||
// String ensures that EventFlags satisfies the fmt.Stringer interface.
|
||||
func (ef EventFlags) String() string {
|
||||
switch {
|
||||
case ef&DOUBLE_CLICK > 0:
|
||||
return "DoubleClick"
|
||||
case ef&MOUSE_WHEELED > 0:
|
||||
return "Wheeled"
|
||||
case ef&MOUSE_MOVED > 0:
|
||||
return "Moved"
|
||||
case ef&MOUSE_HWHEELED > 0:
|
||||
return "HWheeld"
|
||||
case ef == CLICK:
|
||||
return "Click"
|
||||
default:
|
||||
return fmt.Sprintf("Unknown(%d)", ef)
|
||||
}
|
||||
}
|
||||
|
||||
func (ef EventFlags) Contains(flag EventFlags) bool {
|
||||
return ef&flag > 0
|
||||
}
|
||||
|
||||
// Valid values for EventFlags.
|
||||
const (
|
||||
CLICK EventFlags = 0x0000
|
||||
MOUSE_MOVED EventFlags = 0x0001
|
||||
DOUBLE_CLICK EventFlags = 0x0002
|
||||
MOUSE_WHEELED EventFlags = 0x0004
|
||||
MOUSE_HWHEELED EventFlags = 0x0008
|
||||
)
|
||||
|
||||
func highWord(data uint32) uint16 {
|
||||
return uint16((data & 0xFFFF0000) >> 16)
|
||||
}
|
32
vendor/github.com/leonelquinteros/gotext/.gitignore
generated
vendored
Normal file
32
vendor/github.com/leonelquinteros/gotext/.gitignore
generated
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
# Local IDE
|
||||
.project
|
||||
.settings
|
||||
.buildpath
|
||||
.idea
|
||||
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
cli/xgotext/fixtures/out
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
*.DS_Store
|
46
vendor/github.com/leonelquinteros/gotext/CODE_OF_CONDUCT.md
generated
vendored
Normal file
46
vendor/github.com/leonelquinteros/gotext/CODE_OF_CONDUCT.md
generated
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at leonel.quinteros@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
19
vendor/github.com/leonelquinteros/gotext/CONTRIBUTING.md
generated
vendored
Normal file
19
vendor/github.com/leonelquinteros/gotext/CONTRIBUTING.md
generated
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
# CONTRIBUTING
|
||||
|
||||
This open source project welcomes everybody that wants to contribute to it by implementing new features, fixing bugs, testing, creating documentation or simply talk about it.
|
||||
|
||||
Most contributions will start by creating a new Issue to discuss what is the contribution about and to agree on the steps to move forward.
|
||||
|
||||
## Issues
|
||||
|
||||
All issues reports are welcome. Open a new Issue whenever you want to report a bug, request a change or make a proposal.
|
||||
|
||||
This should be your start point of contribution.
|
||||
|
||||
|
||||
## Pull Requests
|
||||
|
||||
If you have any changes that can be merged, feel free to send a Pull Request.
|
||||
|
||||
Usually, you'd want to create a new Issue to discuss about the change you want to merge and why it's needed or what it solves.
|
||||
|
55
vendor/github.com/leonelquinteros/gotext/LICENSE
generated
vendored
Normal file
55
vendor/github.com/leonelquinteros/gotext/LICENSE
generated
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Leonel Quinteros
|
||||
|
||||
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.
|
||||
|
||||
|
||||
Package `plurals`
|
||||
|
||||
Original:
|
||||
https://github.com/ojii/gettext.go/tree/b6dae1d7af8a8441285e42661565760b530a8a57/pluralforms
|
||||
|
||||
License:
|
||||
https://raw.githubusercontent.com/ojii/gettext.go/b6dae1d7af8a8441285e42661565760b530a8a57/LICENSE
|
||||
|
||||
Copyright (c) 2016, Jonas Obrist
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of Jonas Obrist nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL JONAS OBRIST BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
266
vendor/github.com/leonelquinteros/gotext/README.md
generated
vendored
Normal file
266
vendor/github.com/leonelquinteros/gotext/README.md
generated
vendored
Normal file
@ -0,0 +1,266 @@
|
||||
[](https://github.com/leonelquinteros/gotext)
|
||||
[](LICENSE)
|
||||

|
||||
[](https://goreportcard.com/report/github.com/leonelquinteros/gotext)
|
||||
[](https://pkg.go.dev/github.com/leonelquinteros/gotext)
|
||||
|
||||
|
||||
# Gotext
|
||||
|
||||
[GNU gettext utilities](https://www.gnu.org/software/gettext) for Go.
|
||||
|
||||
|
||||
# Features
|
||||
|
||||
- Implements GNU gettext support in native Go.
|
||||
- Complete support for [PO files](https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html) including:
|
||||
- Support for multiline strings and headers.
|
||||
- Support for variables inside translation strings using Go's [fmt syntax](https://golang.org/pkg/fmt/).
|
||||
- Support for [pluralization rules](https://www.gnu.org/software/gettext/manual/html_node/Translating-plural-forms.html).
|
||||
- Support for [message contexts](https://www.gnu.org/software/gettext/manual/html_node/Contexts.html).
|
||||
- Support for MO files.
|
||||
- Thread-safe: This package is safe for concurrent use across multiple goroutines.
|
||||
- It works with UTF-8 encoding as it's the default for Go language.
|
||||
- Unit tests available.
|
||||
- Language codes are automatically simplified from the form `en_UK` to `en` if the first isn't available.
|
||||
- Ready to use inside Go templates.
|
||||
- Objects are serializable to []byte to store them in cache.
|
||||
- Support for Go Modules.
|
||||
|
||||
|
||||
# License
|
||||
|
||||
[MIT license](LICENSE)
|
||||
|
||||
|
||||
# Documentation
|
||||
|
||||
Refer to package documentation at (https://pkg.go.dev/github.com/leonelquinteros/gotext)
|
||||
|
||||
|
||||
# Installation
|
||||
|
||||
```
|
||||
go get github.com/leonelquinteros/gotext
|
||||
```
|
||||
|
||||
- There are no requirements or dependencies to use this package.
|
||||
- No need to install GNU gettext utilities (unless specific needs of CLI tools).
|
||||
- No need for environment variables. Some naming conventions are applied but not needed.
|
||||
|
||||
|
||||
# Usage examples
|
||||
|
||||
## Using package for single language/domain settings
|
||||
|
||||
For quick/simple translations you can use the package level functions directly.
|
||||
|
||||
```go
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Configure package
|
||||
gotext.Configure("/path/to/locales/root/dir", "en_UK", "domain-name")
|
||||
|
||||
// Translate text from default domain
|
||||
fmt.Println(gotext.Get("My text on 'domain-name' domain"))
|
||||
|
||||
// Translate text from a different domain without reconfigure
|
||||
fmt.Println(gotext.GetD("domain2", "Another text on a different domain"))
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Using dynamic variables on translations
|
||||
|
||||
All translation strings support dynamic variables to be inserted without translate.
|
||||
Use the fmt.Printf syntax (from Go's "fmt" package) to specify how to print the non-translated variable inside the translation string.
|
||||
|
||||
```go
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Configure package
|
||||
gotext.Configure("/path/to/locales/root/dir", "en_UK", "domain-name")
|
||||
|
||||
// Set variables
|
||||
name := "John"
|
||||
|
||||
// Translate text with variables
|
||||
fmt.Println(gotext.Get("Hi, my name is %s", name))
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Using Locale object
|
||||
|
||||
When having multiple languages/domains/libraries at the same time, you can create Locale objects for each variation
|
||||
so you can handle each settings on their own.
|
||||
|
||||
```go
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create Locale with library path and language code
|
||||
l := gotext.NewLocale("/path/to/locales/root/dir", "es_UY")
|
||||
|
||||
// Load domain '/path/to/locales/root/dir/es_UY/default.po'
|
||||
l.AddDomain("default")
|
||||
|
||||
// Translate text from default domain
|
||||
fmt.Println(l.Get("Translate this"))
|
||||
|
||||
// Load different domain
|
||||
l.AddDomain("translations")
|
||||
|
||||
// Translate text from domain
|
||||
fmt.Println(l.GetD("translations", "Translate this"))
|
||||
}
|
||||
```
|
||||
|
||||
This is also helpful for using inside templates (from the "text/template" package), where you can pass the Locale object to the template.
|
||||
If you set the Locale object as "Loc" in the template, then the template code would look like:
|
||||
|
||||
```
|
||||
{{ .Loc.Get "Translate this" }}
|
||||
```
|
||||
|
||||
|
||||
## Using the Po object to handle .po files and PO-formatted strings
|
||||
|
||||
For when you need to work with PO files and strings,
|
||||
you can directly use the Po object to parse it and access the translations in there in the same way.
|
||||
|
||||
```go
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Set PO content
|
||||
str := `
|
||||
msgid "Translate this"
|
||||
msgstr "Translated text"
|
||||
|
||||
msgid "Another string"
|
||||
msgstr ""
|
||||
|
||||
msgid "One with var: %s"
|
||||
msgstr "This one sets the var: %s"
|
||||
`
|
||||
|
||||
// Create Po object
|
||||
po := gotext.NewPo()
|
||||
po.Parse(str)
|
||||
|
||||
fmt.Println(po.Get("Translate this"))
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Use plural forms of translations
|
||||
|
||||
PO format supports defining one or more plural forms for the same translation.
|
||||
Relying on the PO file headers, a Plural-Forms formula can be set on the translation file
|
||||
as defined in (https://www.gnu.org/savannah-checkouts/gnu/gettext/manual/html_node/Plural-forms.html)
|
||||
|
||||
```go
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Set PO content
|
||||
str := `
|
||||
msgid ""
|
||||
msgstr ""
|
||||
|
||||
# Header below
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
msgid "Translate this"
|
||||
msgstr "Translated text"
|
||||
|
||||
msgid "Another string"
|
||||
msgstr ""
|
||||
|
||||
msgid "One with var: %s"
|
||||
msgid_plural "Several with vars: %s"
|
||||
msgstr[0] "This one is the singular: %s"
|
||||
msgstr[1] "This one is the plural: %s"
|
||||
`
|
||||
|
||||
// Create Po object
|
||||
po := new(gotext.Po)
|
||||
po.Parse(str)
|
||||
|
||||
fmt.Println(po.GetN("One with var: %s", "Several with vars: %s", 54, v))
|
||||
// "This one is the plural: Variable"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
# Locales directories structure
|
||||
|
||||
The package will assume a directories structure starting with a base path that will be provided to the package configuration
|
||||
or to object constructors depending on the use, but either will use the same convention to lookup inside the base path.
|
||||
|
||||
Inside the base directory where will be the language directories named using the language and country 2-letter codes (en_US, es_AR, ...).
|
||||
All package functions can lookup after the simplified version for each language in case the full code isn't present but the more general language code exists.
|
||||
So if the language set is `en_UK`, but there is no directory named after that code and there is a directory named `en`,
|
||||
all package functions will be able to resolve this generalization and provide translations for the more general library.
|
||||
|
||||
The language codes are assumed to be [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) codes (2-letter codes).
|
||||
That said, most functions will work with any coding standard as long the directory name matches the language code set on the configuration.
|
||||
|
||||
Then, there can be a `LC_MESSAGES` containing all PO files or the PO files themselves.
|
||||
A library directory structure can look like:
|
||||
|
||||
```
|
||||
/path/to/locales
|
||||
/path/to/locales/en_US
|
||||
/path/to/locales/en_US/LC_MESSAGES
|
||||
/path/to/locales/en_US/LC_MESSAGES/default.po
|
||||
/path/to/locales/en_US/LC_MESSAGES/extras.po
|
||||
/path/to/locales/en_UK
|
||||
/path/to/locales/en_UK/LC_MESSAGES
|
||||
/path/to/locales/en_UK/LC_MESSAGES/default.po
|
||||
/path/to/locales/en_UK/LC_MESSAGES/extras.po
|
||||
/path/to/locales/en_AU
|
||||
/path/to/locales/en_AU/LC_MESSAGES
|
||||
/path/to/locales/en_AU/LC_MESSAGES/default.po
|
||||
/path/to/locales/en_AU/LC_MESSAGES/extras.po
|
||||
/path/to/locales/es
|
||||
/path/to/locales/es/default.po
|
||||
/path/to/locales/es/extras.po
|
||||
/path/to/locales/es_ES
|
||||
/path/to/locales/es_ES/default.po
|
||||
/path/to/locales/es_ES/extras.po
|
||||
/path/to/locales/fr
|
||||
/path/to/locales/fr/default.po
|
||||
/path/to/locales/fr/extras.po
|
||||
```
|
||||
|
||||
And so on...
|
||||
|
||||
|
||||
# Contribute
|
||||
|
||||
- Please, contribute.
|
||||
- Use the package on your projects.
|
||||
- Report issues on Github.
|
||||
- Send pull requests for bugfixes and improvements.
|
||||
- Send proposals on Github issues.
|
||||
|
866
vendor/github.com/leonelquinteros/gotext/domain.go
generated
vendored
Normal file
866
vendor/github.com/leonelquinteros/gotext/domain.go
generated
vendored
Normal file
@ -0,0 +1,866 @@
|
||||
package gotext
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/leonelquinteros/gotext/plurals"
|
||||
)
|
||||
|
||||
// Domain has all the common functions for dealing with a gettext domain
|
||||
// it's initialized with a GettextFile (which represents either a Po or Mo file)
|
||||
type Domain struct {
|
||||
Headers HeaderMap
|
||||
|
||||
// Language header
|
||||
Language string
|
||||
|
||||
// Plural-Forms header
|
||||
PluralForms string
|
||||
|
||||
// Preserve comments at head of PO for round-trip
|
||||
headerComments []string
|
||||
|
||||
// Parsed Plural-Forms header values
|
||||
nplurals int
|
||||
plural string
|
||||
pluralforms plurals.Expression
|
||||
|
||||
// Storage
|
||||
translations map[string]*Translation
|
||||
contextTranslations map[string]map[string]*Translation
|
||||
pluralTranslations map[string]*Translation
|
||||
|
||||
// Sync Mutex
|
||||
trMutex sync.RWMutex
|
||||
pluralMutex sync.RWMutex
|
||||
|
||||
// Parsing buffers
|
||||
trBuffer *Translation
|
||||
ctxBuffer string
|
||||
refBuffer string
|
||||
|
||||
customPluralResolver func(int) int
|
||||
}
|
||||
|
||||
// HeaderMap preserves MIMEHeader behaviour, without the canonicalisation
|
||||
type HeaderMap map[string][]string
|
||||
|
||||
// Add key/value pair to HeaderMap
|
||||
func (m HeaderMap) Add(key, value string) {
|
||||
m[key] = append(m[key], value)
|
||||
}
|
||||
|
||||
// Del key from HeaderMap
|
||||
func (m HeaderMap) Del(key string) {
|
||||
delete(m, key)
|
||||
}
|
||||
|
||||
// Get value for key from HeaderMap
|
||||
func (m HeaderMap) Get(key string) string {
|
||||
if m == nil {
|
||||
return ""
|
||||
}
|
||||
v := m[key]
|
||||
if len(v) == 0 {
|
||||
return ""
|
||||
}
|
||||
return v[0]
|
||||
}
|
||||
|
||||
// Set key/value pair in HeaderMap
|
||||
func (m HeaderMap) Set(key, value string) {
|
||||
m[key] = []string{value}
|
||||
}
|
||||
|
||||
// Values returns all values for a given key from HeaderMap
|
||||
func (m HeaderMap) Values(key string) []string {
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
return m[key]
|
||||
}
|
||||
|
||||
// NewDomain creates a new Domain instance
|
||||
func NewDomain() *Domain {
|
||||
domain := new(Domain)
|
||||
|
||||
domain.Headers = make(HeaderMap)
|
||||
domain.headerComments = make([]string, 0)
|
||||
domain.translations = make(map[string]*Translation)
|
||||
domain.contextTranslations = make(map[string]map[string]*Translation)
|
||||
domain.pluralTranslations = make(map[string]*Translation)
|
||||
|
||||
return domain
|
||||
}
|
||||
|
||||
// SetPluralResolver sets a custom plural resolver function
|
||||
func (do *Domain) SetPluralResolver(f func(int) int) {
|
||||
do.customPluralResolver = f
|
||||
}
|
||||
|
||||
func (do *Domain) pluralForm(n int) int {
|
||||
// do we really need locking here? not sure how this plurals.Expression works, so sticking with it for now
|
||||
do.pluralMutex.RLock()
|
||||
defer do.pluralMutex.RUnlock()
|
||||
|
||||
// Failure fallback
|
||||
if do.pluralforms == nil {
|
||||
if do.customPluralResolver != nil {
|
||||
return do.customPluralResolver(n)
|
||||
}
|
||||
|
||||
/* Use the Germanic plural rule. */
|
||||
if n == 1 {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
}
|
||||
return do.pluralforms.Eval(uint32(n))
|
||||
}
|
||||
|
||||
// parseHeaders retrieves data from previously parsed headers. it's called by both Mo and Po when parsing
|
||||
func (do *Domain) parseHeaders() {
|
||||
raw := ""
|
||||
if _, ok := do.translations[raw]; ok {
|
||||
raw = do.translations[raw].Get()
|
||||
}
|
||||
|
||||
// textproto.ReadMIMEHeader() forces keys through CanonicalMIMEHeaderKey(); must read header manually to have one-to-one round-trip of keys
|
||||
languageKey := "Language"
|
||||
pluralFormsKey := "Plural-Forms"
|
||||
|
||||
rawLines := strings.Split(raw, "\n")
|
||||
for _, line := range rawLines {
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
colonIdx := strings.Index(line, ":")
|
||||
if colonIdx < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
key := line[:colonIdx]
|
||||
lowerKey := strings.ToLower(key)
|
||||
if lowerKey == strings.ToLower(languageKey) {
|
||||
languageKey = key
|
||||
} else if lowerKey == strings.ToLower(pluralFormsKey) {
|
||||
pluralFormsKey = key
|
||||
}
|
||||
|
||||
value := strings.TrimSpace(line[colonIdx+1:])
|
||||
do.Headers.Add(key, value)
|
||||
}
|
||||
|
||||
// Get/save needed headers
|
||||
do.Language = do.Headers.Get(languageKey)
|
||||
do.PluralForms = do.Headers.Get(pluralFormsKey)
|
||||
|
||||
// Parse Plural-Forms formula
|
||||
if do.PluralForms == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Split plural form header value
|
||||
pfs := strings.Split(do.PluralForms, ";")
|
||||
|
||||
// Parse values
|
||||
for _, i := range pfs {
|
||||
vs := strings.SplitN(i, "=", 2)
|
||||
if len(vs) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
switch strings.TrimSpace(vs[0]) {
|
||||
case "nplurals":
|
||||
do.nplurals, _ = strconv.Atoi(vs[1])
|
||||
|
||||
case "plural":
|
||||
do.plural = vs[1]
|
||||
|
||||
if expr, err := plurals.Compile(do.plural); err == nil {
|
||||
do.pluralforms = expr
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DropStaleTranslations drops any translations stored that have not been Set*()
|
||||
// since 'po' was initialised
|
||||
func (do *Domain) DropStaleTranslations() {
|
||||
do.trMutex.Lock()
|
||||
do.pluralMutex.Lock()
|
||||
defer do.trMutex.Unlock()
|
||||
defer do.pluralMutex.Unlock()
|
||||
|
||||
for name, ctx := range do.contextTranslations {
|
||||
for id, trans := range ctx {
|
||||
if trans.IsStale() {
|
||||
delete(ctx, id)
|
||||
}
|
||||
}
|
||||
if len(ctx) == 0 {
|
||||
delete(do.contextTranslations, name)
|
||||
}
|
||||
}
|
||||
|
||||
for id, trans := range do.translations {
|
||||
if trans.IsStale() {
|
||||
delete(do.translations, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetRefs set source references for a given translation
|
||||
func (do *Domain) SetRefs(str string, refs []string) {
|
||||
do.trMutex.Lock()
|
||||
do.pluralMutex.Lock()
|
||||
defer do.trMutex.Unlock()
|
||||
defer do.pluralMutex.Unlock()
|
||||
|
||||
if trans, ok := do.translations[str]; ok {
|
||||
trans.Refs = refs
|
||||
} else {
|
||||
trans = NewTranslation()
|
||||
trans.ID = str
|
||||
trans.SetRefs(refs)
|
||||
do.translations[str] = trans
|
||||
}
|
||||
}
|
||||
|
||||
// GetRefs get source references for a given translation
|
||||
func (do *Domain) GetRefs(str string) []string {
|
||||
// Sync read
|
||||
do.trMutex.RLock()
|
||||
defer do.trMutex.RUnlock()
|
||||
|
||||
if do.translations != nil {
|
||||
if trans, ok := do.translations[str]; ok {
|
||||
return trans.Refs
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set the translation of a given string
|
||||
func (do *Domain) Set(id, str string) {
|
||||
do.trMutex.Lock()
|
||||
do.pluralMutex.Lock()
|
||||
defer do.trMutex.Unlock()
|
||||
defer do.pluralMutex.Unlock()
|
||||
|
||||
if trans, ok := do.translations[id]; ok {
|
||||
trans.Set(str)
|
||||
} else {
|
||||
trans = NewTranslation()
|
||||
trans.ID = id
|
||||
trans.Set(str)
|
||||
do.translations[id] = trans
|
||||
}
|
||||
}
|
||||
|
||||
// Get retrieves the Translation for the given string.
|
||||
func (do *Domain) Get(str string, vars ...interface{}) string {
|
||||
// Sync read
|
||||
do.trMutex.RLock()
|
||||
defer do.trMutex.RUnlock()
|
||||
|
||||
if do.translations != nil {
|
||||
if _, ok := do.translations[str]; ok {
|
||||
return FormatString(do.translations[str].Get(), vars...)
|
||||
}
|
||||
}
|
||||
|
||||
// Return the same we received by default
|
||||
return FormatString(str, vars...)
|
||||
}
|
||||
|
||||
// Append retrieves the Translation for the given string.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (do *Domain) Append(b []byte, str string, vars ...interface{}) []byte {
|
||||
// Sync read
|
||||
do.trMutex.RLock()
|
||||
defer do.trMutex.RUnlock()
|
||||
|
||||
if do.translations != nil {
|
||||
if _, ok := do.translations[str]; ok {
|
||||
return Appendf(b, do.translations[str].Get(), vars...)
|
||||
}
|
||||
}
|
||||
|
||||
// Return the same we received by default
|
||||
return Appendf(b, str, vars...)
|
||||
}
|
||||
|
||||
// SetN sets the (N)th plural form for the given string
|
||||
func (do *Domain) SetN(id, plural string, n int, str string) {
|
||||
// Get plural form _before_ lock down
|
||||
pluralForm := do.pluralForm(n)
|
||||
|
||||
do.trMutex.Lock()
|
||||
do.pluralMutex.Lock()
|
||||
defer do.trMutex.Unlock()
|
||||
defer do.pluralMutex.Unlock()
|
||||
|
||||
if trans, ok := do.translations[id]; ok {
|
||||
trans.SetN(pluralForm, str)
|
||||
} else {
|
||||
trans = NewTranslation()
|
||||
trans.ID = id
|
||||
trans.PluralID = plural
|
||||
trans.SetN(pluralForm, str)
|
||||
do.translations[id] = trans
|
||||
}
|
||||
}
|
||||
|
||||
// GetN retrieves the (N)th plural form of Translation for the given string.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (do *Domain) GetN(str, plural string, n int, vars ...interface{}) string {
|
||||
// Sync read
|
||||
do.trMutex.RLock()
|
||||
defer do.trMutex.RUnlock()
|
||||
|
||||
if do.translations != nil {
|
||||
if _, ok := do.translations[str]; ok {
|
||||
return FormatString(do.translations[str].GetN(do.pluralForm(n)), vars...)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse plural forms to distinguish between plural and singular
|
||||
if do.pluralForm(n) == 0 {
|
||||
return FormatString(str, vars...)
|
||||
}
|
||||
return FormatString(plural, vars...)
|
||||
}
|
||||
|
||||
// AppendN adds the (N)th plural form of Translation for the given string.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (do *Domain) AppendN(b []byte, str, plural string, n int, vars ...interface{}) []byte {
|
||||
// Sync read
|
||||
do.trMutex.RLock()
|
||||
defer do.trMutex.RUnlock()
|
||||
|
||||
if do.translations != nil {
|
||||
if _, ok := do.translations[str]; ok {
|
||||
return Appendf(b, do.translations[str].GetN(do.pluralForm(n)), vars...)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse plural forms to distinguish between plural and singular
|
||||
if do.pluralForm(n) == 0 {
|
||||
return Appendf(b, str, vars...)
|
||||
}
|
||||
return Appendf(b, plural, vars...)
|
||||
}
|
||||
|
||||
// SetC sets the translation for the given string in the given context
|
||||
func (do *Domain) SetC(id, ctx, str string) {
|
||||
do.trMutex.Lock()
|
||||
do.pluralMutex.Lock()
|
||||
defer do.trMutex.Unlock()
|
||||
defer do.pluralMutex.Unlock()
|
||||
|
||||
if context, ok := do.contextTranslations[ctx]; ok {
|
||||
if trans, hasTrans := context[id]; hasTrans {
|
||||
trans.Set(str)
|
||||
} else {
|
||||
trans = NewTranslation()
|
||||
trans.ID = id
|
||||
trans.Set(str)
|
||||
context[id] = trans
|
||||
}
|
||||
} else {
|
||||
trans := NewTranslation()
|
||||
trans.ID = id
|
||||
trans.Set(str)
|
||||
do.contextTranslations[ctx] = map[string]*Translation{
|
||||
id: trans,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetC retrieves the corresponding Translation for a given string in the given context.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (do *Domain) GetC(str, ctx string, vars ...interface{}) string {
|
||||
do.trMutex.RLock()
|
||||
defer do.trMutex.RUnlock()
|
||||
|
||||
if do.contextTranslations != nil {
|
||||
if _, ok := do.contextTranslations[ctx]; ok {
|
||||
if do.contextTranslations[ctx] != nil {
|
||||
if _, ok := do.contextTranslations[ctx][str]; ok {
|
||||
return FormatString(do.contextTranslations[ctx][str].Get(), vars...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the string we received by default
|
||||
return FormatString(str, vars...)
|
||||
}
|
||||
|
||||
// AppendC retrieves the corresponding Translation for a given string in the given context.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (do *Domain) AppendC(b []byte, str, ctx string, vars ...interface{}) []byte {
|
||||
do.trMutex.RLock()
|
||||
defer do.trMutex.RUnlock()
|
||||
|
||||
if do.contextTranslations != nil {
|
||||
if _, ok := do.contextTranslations[ctx]; ok {
|
||||
if do.contextTranslations[ctx] != nil {
|
||||
if _, ok := do.contextTranslations[ctx][str]; ok {
|
||||
return Appendf(b, do.contextTranslations[ctx][str].Get(), vars...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the string we received by default
|
||||
return Appendf(b, str, vars...)
|
||||
}
|
||||
|
||||
// SetNC sets the (N)th plural form for the given string in the given context
|
||||
func (do *Domain) SetNC(id, plural, ctx string, n int, str string) {
|
||||
// Get plural form _before_ lock down
|
||||
pluralForm := do.pluralForm(n)
|
||||
|
||||
do.trMutex.Lock()
|
||||
do.pluralMutex.Lock()
|
||||
defer do.trMutex.Unlock()
|
||||
defer do.pluralMutex.Unlock()
|
||||
|
||||
if context, ok := do.contextTranslations[ctx]; ok {
|
||||
if trans, hasTrans := context[id]; hasTrans {
|
||||
trans.SetN(pluralForm, str)
|
||||
} else {
|
||||
trans = NewTranslation()
|
||||
trans.ID = id
|
||||
trans.SetN(pluralForm, str)
|
||||
context[id] = trans
|
||||
}
|
||||
} else {
|
||||
trans := NewTranslation()
|
||||
trans.ID = id
|
||||
trans.SetN(pluralForm, str)
|
||||
do.contextTranslations[ctx] = map[string]*Translation{
|
||||
id: trans,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetNC retrieves the (N)th plural form of Translation for the given string in the given context.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (do *Domain) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
|
||||
do.trMutex.RLock()
|
||||
defer do.trMutex.RUnlock()
|
||||
|
||||
if do.contextTranslations != nil {
|
||||
if _, ok := do.contextTranslations[ctx]; ok {
|
||||
if do.contextTranslations[ctx] != nil {
|
||||
if _, ok := do.contextTranslations[ctx][str]; ok {
|
||||
return FormatString(do.contextTranslations[ctx][str].GetN(do.pluralForm(n)), vars...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if n == 1 {
|
||||
return FormatString(str, vars...)
|
||||
}
|
||||
return FormatString(plural, vars...)
|
||||
}
|
||||
|
||||
// AppendNC retrieves the (N)th plural form of Translation for the given string in the given context.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (do *Domain) AppendNC(b []byte, str, plural string, n int, ctx string, vars ...interface{}) []byte {
|
||||
do.trMutex.RLock()
|
||||
defer do.trMutex.RUnlock()
|
||||
|
||||
if do.contextTranslations != nil {
|
||||
if _, ok := do.contextTranslations[ctx]; ok {
|
||||
if do.contextTranslations[ctx] != nil {
|
||||
if _, ok := do.contextTranslations[ctx][str]; ok {
|
||||
return Appendf(b, do.contextTranslations[ctx][str].GetN(do.pluralForm(n)), vars...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if n == 1 {
|
||||
return Appendf(b, str, vars...)
|
||||
}
|
||||
return Appendf(b, plural, vars...)
|
||||
}
|
||||
|
||||
// IsTranslated reports whether a string is translated
|
||||
func (do *Domain) IsTranslated(str string) bool {
|
||||
return do.IsTranslatedN(str, 1)
|
||||
}
|
||||
|
||||
// IsTranslatedN reports whether a plural string is translated
|
||||
func (do *Domain) IsTranslatedN(str string, n int) bool {
|
||||
do.trMutex.RLock()
|
||||
defer do.trMutex.RUnlock()
|
||||
|
||||
if do.translations == nil {
|
||||
return false
|
||||
}
|
||||
tr, ok := do.translations[str]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return tr.IsTranslatedN(do.pluralForm(n))
|
||||
}
|
||||
|
||||
// IsTranslatedC reports whether a context string is translated
|
||||
func (do *Domain) IsTranslatedC(str, ctx string) bool {
|
||||
return do.IsTranslatedNC(str, 1, ctx)
|
||||
}
|
||||
|
||||
// IsTranslatedNC reports whether a plural context string is translated
|
||||
func (do *Domain) IsTranslatedNC(str string, n int, ctx string) bool {
|
||||
do.trMutex.RLock()
|
||||
defer do.trMutex.RUnlock()
|
||||
|
||||
if do.contextTranslations == nil {
|
||||
return false
|
||||
}
|
||||
translations, ok := do.contextTranslations[ctx]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
tr, ok := translations[str]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return tr.IsTranslatedN(do.pluralForm(n))
|
||||
}
|
||||
|
||||
// GetTranslations returns a copy of every translation in the domain. It does not support contexts.
|
||||
func (do *Domain) GetTranslations() map[string]*Translation {
|
||||
all := make(map[string]*Translation, len(do.translations))
|
||||
|
||||
do.trMutex.RLock()
|
||||
defer do.trMutex.RUnlock()
|
||||
|
||||
for msgID, trans := range do.translations {
|
||||
newTrans := NewTranslation()
|
||||
newTrans.ID = trans.ID
|
||||
newTrans.PluralID = trans.PluralID
|
||||
newTrans.dirty = trans.dirty
|
||||
if len(trans.Refs) > 0 {
|
||||
newTrans.Refs = make([]string, len(trans.Refs))
|
||||
copy(newTrans.Refs, trans.Refs)
|
||||
}
|
||||
for k, v := range trans.Trs {
|
||||
newTrans.Trs[k] = v
|
||||
}
|
||||
all[msgID] = newTrans
|
||||
}
|
||||
|
||||
return all
|
||||
}
|
||||
|
||||
// GetCtxTranslations returns a copy of every translation in the domain with context
|
||||
func (do *Domain) GetCtxTranslations() map[string]map[string]*Translation {
|
||||
all := make(map[string]map[string]*Translation, len(do.contextTranslations))
|
||||
|
||||
do.trMutex.RLock()
|
||||
defer do.trMutex.RUnlock()
|
||||
|
||||
for ctx, translations := range do.contextTranslations {
|
||||
for msgID, trans := range translations {
|
||||
newTrans := NewTranslation()
|
||||
newTrans.ID = trans.ID
|
||||
newTrans.PluralID = trans.PluralID
|
||||
newTrans.dirty = trans.dirty
|
||||
if len(trans.Refs) > 0 {
|
||||
newTrans.Refs = make([]string, len(trans.Refs))
|
||||
copy(newTrans.Refs, trans.Refs)
|
||||
}
|
||||
for k, v := range trans.Trs {
|
||||
newTrans.Trs[k] = v
|
||||
}
|
||||
|
||||
if all[ctx] == nil {
|
||||
all[ctx] = make(map[string]*Translation)
|
||||
}
|
||||
|
||||
all[ctx][msgID] = newTrans
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return all
|
||||
}
|
||||
|
||||
// SourceReference is a struct to hold source reference information
|
||||
type SourceReference struct {
|
||||
path string
|
||||
line int
|
||||
context string
|
||||
trans *Translation
|
||||
}
|
||||
|
||||
func extractPathAndLine(ref string) (string, int) {
|
||||
var path string
|
||||
var line int
|
||||
colonIdx := strings.IndexRune(ref, ':')
|
||||
if colonIdx >= 0 {
|
||||
path = ref[:colonIdx]
|
||||
line, _ = strconv.Atoi(ref[colonIdx+1:])
|
||||
} else {
|
||||
path = ref
|
||||
line = 0
|
||||
}
|
||||
return path, line
|
||||
}
|
||||
|
||||
// MarshalText implements encoding.TextMarshaler interface
|
||||
// Assists round-trip of POT/PO content
|
||||
func (do *Domain) MarshalText() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
if len(do.headerComments) > 0 {
|
||||
buf.WriteString(strings.Join(do.headerComments, "\n"))
|
||||
buf.WriteByte(byte('\n'))
|
||||
}
|
||||
buf.WriteString("msgid \"\"\nmsgstr \"\"")
|
||||
|
||||
// Standard order consistent with xgettext
|
||||
headerOrder := map[string]int{
|
||||
"project-id-version": 0,
|
||||
"report-msgid-bugs-to": 1,
|
||||
"pot-creation-date": 2,
|
||||
"po-revision-date": 3,
|
||||
"last-translator": 4,
|
||||
"language-team": 5,
|
||||
"language": 6,
|
||||
"mime-version": 7,
|
||||
"content-type": 9,
|
||||
"content-transfer-encoding": 10,
|
||||
"plural-forms": 11,
|
||||
}
|
||||
|
||||
headerKeys := make([]string, 0, len(do.Headers))
|
||||
|
||||
for k := range do.Headers {
|
||||
headerKeys = append(headerKeys, k)
|
||||
}
|
||||
|
||||
sort.Slice(headerKeys, func(i, j int) bool {
|
||||
var iOrder int
|
||||
var jOrder int
|
||||
var ok bool
|
||||
if iOrder, ok = headerOrder[strings.ToLower(headerKeys[i])]; !ok {
|
||||
iOrder = 8
|
||||
}
|
||||
|
||||
if jOrder, ok = headerOrder[strings.ToLower(headerKeys[j])]; !ok {
|
||||
jOrder = 8
|
||||
}
|
||||
|
||||
if iOrder < jOrder {
|
||||
return true
|
||||
}
|
||||
if iOrder > jOrder {
|
||||
return false
|
||||
}
|
||||
return headerKeys[i] < headerKeys[j]
|
||||
})
|
||||
|
||||
for _, k := range headerKeys {
|
||||
// Access Headers map directly so as not to canonicalise
|
||||
v := do.Headers[k]
|
||||
|
||||
for _, value := range v {
|
||||
buf.WriteString("\n\"" + k + ": " + value + "\\n\"")
|
||||
}
|
||||
}
|
||||
|
||||
// Just as with headers, output translations in consistent order (to minimise diffs between round-trips), with (first) source reference taking priority, followed by context and finally ID
|
||||
references := make([]SourceReference, 0)
|
||||
for name, ctx := range do.contextTranslations {
|
||||
for id, trans := range ctx {
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
if len(trans.Refs) > 0 {
|
||||
path, line := extractPathAndLine(trans.Refs[0])
|
||||
references = append(references, SourceReference{
|
||||
path,
|
||||
line,
|
||||
name,
|
||||
trans,
|
||||
})
|
||||
} else {
|
||||
references = append(references, SourceReference{
|
||||
"",
|
||||
0,
|
||||
name,
|
||||
trans,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for id, trans := range do.translations {
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(trans.Refs) > 0 {
|
||||
path, line := extractPathAndLine(trans.Refs[0])
|
||||
references = append(references, SourceReference{
|
||||
path,
|
||||
line,
|
||||
"",
|
||||
trans,
|
||||
})
|
||||
} else {
|
||||
references = append(references, SourceReference{
|
||||
"",
|
||||
0,
|
||||
"",
|
||||
trans,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(references, func(i, j int) bool {
|
||||
if references[i].path < references[j].path {
|
||||
return true
|
||||
}
|
||||
if references[i].path > references[j].path {
|
||||
return false
|
||||
}
|
||||
if references[i].line < references[j].line {
|
||||
return true
|
||||
}
|
||||
if references[i].line > references[j].line {
|
||||
return false
|
||||
}
|
||||
|
||||
if references[i].context < references[j].context {
|
||||
return true
|
||||
}
|
||||
if references[i].context > references[j].context {
|
||||
return false
|
||||
}
|
||||
return references[i].trans.ID < references[j].trans.ID
|
||||
})
|
||||
|
||||
for _, ref := range references {
|
||||
trans := ref.trans
|
||||
if len(trans.Refs) > 0 {
|
||||
buf.WriteString("\n\n#: " + strings.Join(trans.Refs, " "))
|
||||
} else {
|
||||
buf.WriteByte(byte('\n'))
|
||||
}
|
||||
|
||||
if ref.context == "" {
|
||||
buf.WriteString("\nmsgid \"" + EscapeSpecialCharacters(trans.ID) + "\"")
|
||||
} else {
|
||||
buf.WriteString("\nmsgctxt \"" + EscapeSpecialCharacters(ref.context) + "\"\nmsgid \"" + EscapeSpecialCharacters(trans.ID) + "\"")
|
||||
}
|
||||
|
||||
if trans.PluralID == "" {
|
||||
buf.WriteString("\nmsgstr \"" + EscapeSpecialCharacters(trans.Trs[0]) + "\"")
|
||||
} else {
|
||||
buf.WriteString("\nmsgid_plural \"" + trans.PluralID + "\"")
|
||||
for i, tr := range trans.Trs {
|
||||
buf.WriteString("\nmsgstr[" + EscapeSpecialCharacters(strconv.Itoa(i)) + "] \"" + tr + "\"")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// EscapeSpecialCharacters escapes special characters in a string
|
||||
func EscapeSpecialCharacters(s string) string {
|
||||
s = regexp.MustCompile(`([^\\])(")`).ReplaceAllString(s, "$1\\\"") // Escape non-escaped double quotation marks
|
||||
|
||||
if strings.Count(s, "\n") == 0 {
|
||||
return s
|
||||
}
|
||||
|
||||
// Handle EOL and multi-lines
|
||||
// Only one line, but finishing with \n
|
||||
if strings.Count(s, "\n") == 1 && strings.HasSuffix(s, "\n") {
|
||||
return strings.ReplaceAll(s, "\n", "\\n")
|
||||
}
|
||||
|
||||
elems := strings.Split(s, "\n")
|
||||
// Skip last element for multiline which is an empty
|
||||
var shouldEndWithEOL bool
|
||||
if elems[len(elems)-1] == "" {
|
||||
elems = elems[:len(elems)-1]
|
||||
shouldEndWithEOL = true
|
||||
}
|
||||
data := []string{(`"`)}
|
||||
for i, v := range elems {
|
||||
l := fmt.Sprintf(`"%s\n"`, v)
|
||||
// Last element without EOL
|
||||
if i == len(elems)-1 && !shouldEndWithEOL {
|
||||
l = fmt.Sprintf(`"%s"`, v)
|
||||
}
|
||||
// Remove finale " to last element as the whole string will be quoted
|
||||
if i == len(elems)-1 {
|
||||
l = strings.TrimSuffix(l, `"`)
|
||||
}
|
||||
data = append(data, l)
|
||||
}
|
||||
return strings.Join(data, "\n")
|
||||
}
|
||||
|
||||
// MarshalBinary implements encoding.BinaryMarshaler interface
|
||||
func (do *Domain) MarshalBinary() ([]byte, error) {
|
||||
obj := new(TranslatorEncoding)
|
||||
obj.Headers = do.Headers
|
||||
obj.Language = do.Language
|
||||
obj.PluralForms = do.PluralForms
|
||||
obj.Nplurals = do.nplurals
|
||||
obj.Plural = do.plural
|
||||
obj.Translations = do.translations
|
||||
obj.Contexts = do.contextTranslations
|
||||
|
||||
var buff bytes.Buffer
|
||||
encoder := gob.NewEncoder(&buff)
|
||||
err := encoder.Encode(obj)
|
||||
|
||||
return buff.Bytes(), err
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements encoding.BinaryUnmarshaler interface
|
||||
func (do *Domain) UnmarshalBinary(data []byte) error {
|
||||
buff := bytes.NewBuffer(data)
|
||||
obj := new(TranslatorEncoding)
|
||||
|
||||
decoder := gob.NewDecoder(buff)
|
||||
err := decoder.Decode(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
do.Headers = obj.Headers
|
||||
do.Language = obj.Language
|
||||
do.PluralForms = obj.PluralForms
|
||||
do.nplurals = obj.Nplurals
|
||||
do.plural = obj.Plural
|
||||
do.translations = obj.Translations
|
||||
do.contextTranslations = obj.Contexts
|
||||
|
||||
if expr, err := plurals.Compile(do.plural); err == nil {
|
||||
do.pluralforms = expr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
427
vendor/github.com/leonelquinteros/gotext/gotext.go
generated
vendored
Normal file
427
vendor/github.com/leonelquinteros/gotext/gotext.go
generated
vendored
Normal file
@ -0,0 +1,427 @@
|
||||
/*
|
||||
Package gotext implements GNU gettext utilities.
|
||||
|
||||
For quick/simple translations you can use the package level functions directly.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Configure package
|
||||
gotext.Configure("/path/to/locales/root/dir", "en_UK", "domain-name")
|
||||
|
||||
// Translate text from default domain
|
||||
fmt.Println(gotext.Get("My text on 'domain-name' domain"))
|
||||
|
||||
// Translate text from a different domain without reconfigure
|
||||
fmt.Println(gotext.GetD("domain2", "Another text on a different domain"))
|
||||
}
|
||||
*/
|
||||
package gotext
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Global environment variables
|
||||
type config struct {
|
||||
sync.RWMutex
|
||||
|
||||
// Path to library directory where all locale directories and Translation files are.
|
||||
library string
|
||||
|
||||
// Default domain to look at when no domain is specified. Used by package level functions.
|
||||
domain string
|
||||
|
||||
// Language set.
|
||||
languages []string
|
||||
|
||||
// Storage for package level methods
|
||||
locales []*Locale
|
||||
}
|
||||
|
||||
var globalConfig *config
|
||||
|
||||
// FallbackLocale is the default language to be used when no language is set.
|
||||
var FallbackLocale = "en_US"
|
||||
|
||||
func init() {
|
||||
// Init default configuration
|
||||
globalConfig = &config{
|
||||
domain: "default",
|
||||
languages: []string{FallbackLocale},
|
||||
library: "/usr/local/share/locale",
|
||||
locales: nil,
|
||||
}
|
||||
|
||||
// Register Translator types for gob encoding
|
||||
gob.Register(TranslatorEncoding{})
|
||||
}
|
||||
|
||||
// loadLocales creates a new Locale object for every language (specified using Configure)
|
||||
// at package level based on the configuration of global configuration .
|
||||
// It is called when trying to use Get or GetD methods.
|
||||
func loadLocales(rebuildCache bool) {
|
||||
globalConfig.Lock()
|
||||
|
||||
if globalConfig.locales == nil || rebuildCache {
|
||||
var locales []*Locale
|
||||
for _, language := range globalConfig.languages {
|
||||
locales = append(locales, NewLocale(globalConfig.library, language))
|
||||
}
|
||||
globalConfig.locales = locales
|
||||
}
|
||||
|
||||
for _, locale := range globalConfig.locales {
|
||||
if _, ok := locale.Domains[globalConfig.domain]; !ok || rebuildCache {
|
||||
locale.AddDomain(globalConfig.domain)
|
||||
}
|
||||
locale.SetDomain(globalConfig.domain)
|
||||
}
|
||||
|
||||
globalConfig.Unlock()
|
||||
}
|
||||
|
||||
// GetDomain is the domain getter for the package configuration
|
||||
func GetDomain() string {
|
||||
var dom string
|
||||
globalConfig.RLock()
|
||||
if globalConfig.locales != nil {
|
||||
// All locales have the same domain
|
||||
dom = globalConfig.locales[0].GetDomain()
|
||||
}
|
||||
if dom == "" {
|
||||
dom = globalConfig.domain
|
||||
}
|
||||
globalConfig.RUnlock()
|
||||
|
||||
return dom
|
||||
}
|
||||
|
||||
// SetDomain sets the name for the domain to be used at package level.
|
||||
// It reloads the corresponding Translation file.
|
||||
func SetDomain(dom string) {
|
||||
globalConfig.Lock()
|
||||
globalConfig.domain = dom
|
||||
if globalConfig.locales != nil {
|
||||
for _, locale := range globalConfig.locales {
|
||||
locale.SetDomain(dom)
|
||||
}
|
||||
}
|
||||
globalConfig.Unlock()
|
||||
|
||||
loadLocales(true)
|
||||
}
|
||||
|
||||
// GetLanguage returns the language gotext will translate into.
|
||||
// If multiple languages have been supplied, the first one will be returned.
|
||||
// If no language has been supplied, the fallback will be returned.
|
||||
func GetLanguage() string {
|
||||
languages := GetLanguages()
|
||||
if len(languages) == 0 {
|
||||
return FallbackLocale
|
||||
}
|
||||
return languages[0]
|
||||
}
|
||||
|
||||
// GetLanguages returns all languages that have been supplied.
|
||||
func GetLanguages() []string {
|
||||
globalConfig.RLock()
|
||||
defer globalConfig.RUnlock()
|
||||
return globalConfig.languages
|
||||
}
|
||||
|
||||
// SetLanguage sets the language code (or colon separated language codes) to be used at package level.
|
||||
// It reloads the corresponding Translation file.
|
||||
func SetLanguage(lang string) {
|
||||
globalConfig.Lock()
|
||||
var languages []string
|
||||
for _, language := range strings.Split(lang, ":") {
|
||||
languages = append(languages, SimplifiedLocale(language))
|
||||
}
|
||||
globalConfig.languages = languages
|
||||
globalConfig.Unlock()
|
||||
|
||||
loadLocales(true)
|
||||
}
|
||||
|
||||
// GetLibrary is the library getter for the package configuration
|
||||
func GetLibrary() string {
|
||||
globalConfig.RLock()
|
||||
lib := globalConfig.library
|
||||
globalConfig.RUnlock()
|
||||
|
||||
return lib
|
||||
}
|
||||
|
||||
// SetLibrary sets the root path for the locale directories and files to be used at package level.
|
||||
// It reloads the corresponding Translation file.
|
||||
func SetLibrary(lib string) {
|
||||
globalConfig.Lock()
|
||||
globalConfig.library = lib
|
||||
globalConfig.Unlock()
|
||||
|
||||
loadLocales(true)
|
||||
}
|
||||
|
||||
// GetLocales returns the locales that have been set for the package configuration.
|
||||
func GetLocales() []*Locale {
|
||||
globalConfig.RLock()
|
||||
defer globalConfig.RUnlock()
|
||||
return globalConfig.locales
|
||||
}
|
||||
|
||||
// GetStorage is the locale storage getter for the package configuration.
|
||||
//
|
||||
// Deprecated: Storage has been renamed to Locale for consistency, use GetLocales instead.
|
||||
func GetStorage() *Locale {
|
||||
locales := GetLocales()
|
||||
if len(locales) > 0 {
|
||||
return locales[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetLocales allows for overriding the global Locale objects with ones built manually with
|
||||
// NewLocale(). This makes it possible to attach custom Domain objects from in-memory po/mo.
|
||||
// The library, language and domain of the first Locale will set the default global configuration.
|
||||
func SetLocales(locales []*Locale) {
|
||||
globalConfig.Lock()
|
||||
defer globalConfig.Unlock()
|
||||
|
||||
globalConfig.locales = locales
|
||||
globalConfig.library = locales[0].path
|
||||
globalConfig.domain = locales[0].defaultDomain
|
||||
|
||||
var languages []string
|
||||
for _, locale := range locales {
|
||||
languages = append(languages, locale.lang)
|
||||
}
|
||||
globalConfig.languages = languages
|
||||
}
|
||||
|
||||
// SetStorage allows overriding the global Locale object with one built manually with NewLocale().
|
||||
//
|
||||
// Deprecated: Storage has been renamed to Locale for consistency, use SetLocales instead.
|
||||
func SetStorage(locale *Locale) {
|
||||
SetLocales([]*Locale{locale})
|
||||
}
|
||||
|
||||
// Configure sets all configuration variables to be used at package level and reloads the corresponding Translation file.
|
||||
// It receives the library path, language code and domain name.
|
||||
// This function is recommended to be used when changing more than one setting,
|
||||
// as using each setter will introduce a I/O overhead because the Translation file will be loaded after each set.
|
||||
func Configure(lib, lang, dom string) {
|
||||
globalConfig.Lock()
|
||||
globalConfig.library = lib
|
||||
var languages []string
|
||||
for _, language := range strings.Split(lang, ":") {
|
||||
languages = append(languages, SimplifiedLocale(language))
|
||||
}
|
||||
globalConfig.languages = languages
|
||||
globalConfig.domain = dom
|
||||
globalConfig.Unlock()
|
||||
|
||||
loadLocales(true)
|
||||
}
|
||||
|
||||
// Get uses the default domain globally set to return the corresponding Translation of a given string.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func Get(str string, vars ...interface{}) string {
|
||||
return GetD(GetDomain(), str, vars...)
|
||||
}
|
||||
|
||||
// GetN retrieves the (N)th plural form of Translation for the given string in the default domain.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func GetN(str, plural string, n int, vars ...interface{}) string {
|
||||
return GetND(GetDomain(), str, plural, n, vars...)
|
||||
}
|
||||
|
||||
// GetD returns the corresponding Translation in the given domain for a given string.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func GetD(dom, str string, vars ...interface{}) string {
|
||||
// Try to load default package Locales
|
||||
loadLocales(false)
|
||||
|
||||
globalConfig.RLock()
|
||||
defer globalConfig.RUnlock()
|
||||
|
||||
var tr string
|
||||
for i, locale := range globalConfig.locales {
|
||||
if _, ok := locale.Domains[dom]; !ok {
|
||||
locale.AddDomain(dom)
|
||||
}
|
||||
if !locale.IsTranslatedD(dom, str) && i < (len(globalConfig.locales)-1) {
|
||||
continue
|
||||
}
|
||||
tr = locale.GetD(dom, str, vars...)
|
||||
break
|
||||
}
|
||||
return tr
|
||||
}
|
||||
|
||||
// GetND retrieves the (N)th plural form of Translation in the given domain for a given string.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func GetND(dom, str, plural string, n int, vars ...interface{}) string {
|
||||
// Try to load default package Locales
|
||||
loadLocales(false)
|
||||
|
||||
globalConfig.RLock()
|
||||
defer globalConfig.RUnlock()
|
||||
|
||||
var tr string
|
||||
for i, locale := range globalConfig.locales {
|
||||
if _, ok := locale.Domains[dom]; !ok {
|
||||
locale.AddDomain(dom)
|
||||
}
|
||||
if !locale.IsTranslatedND(dom, str, n) && i < (len(globalConfig.locales)-1) {
|
||||
continue
|
||||
}
|
||||
tr = locale.GetND(dom, str, plural, n, vars...)
|
||||
break
|
||||
}
|
||||
return tr
|
||||
}
|
||||
|
||||
// GetC uses the default domain globally set to return the corresponding Translation of the given string in the given context.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func GetC(str, ctx string, vars ...interface{}) string {
|
||||
return GetDC(GetDomain(), str, ctx, vars...)
|
||||
}
|
||||
|
||||
// GetNC retrieves the (N)th plural form of Translation for the given string in the given context in the default domain.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
|
||||
return GetNDC(GetDomain(), str, plural, n, ctx, vars...)
|
||||
}
|
||||
|
||||
// GetDC returns the corresponding Translation in the given domain for the given string in the given context.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func GetDC(dom, str, ctx string, vars ...interface{}) string {
|
||||
// Try to load default package Locales
|
||||
loadLocales(false)
|
||||
|
||||
globalConfig.RLock()
|
||||
defer globalConfig.RUnlock()
|
||||
|
||||
var tr string
|
||||
for i, locale := range globalConfig.locales {
|
||||
if !locale.IsTranslatedDC(dom, str, ctx) && i < (len(globalConfig.locales)-1) {
|
||||
continue
|
||||
}
|
||||
tr = locale.GetDC(dom, str, ctx, vars...)
|
||||
break
|
||||
}
|
||||
return tr
|
||||
}
|
||||
|
||||
// GetNDC retrieves the (N)th plural form of Translation in the given domain for a given string.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func GetNDC(dom, str, plural string, n int, ctx string, vars ...interface{}) string {
|
||||
// Try to load default package Locales
|
||||
loadLocales(false)
|
||||
|
||||
// Return Translation
|
||||
globalConfig.RLock()
|
||||
defer globalConfig.RUnlock()
|
||||
|
||||
var tr string
|
||||
for i, locale := range globalConfig.locales {
|
||||
if !locale.IsTranslatedNDC(dom, str, n, ctx) && i < (len(globalConfig.locales)-1) {
|
||||
continue
|
||||
}
|
||||
tr = locale.GetNDC(dom, str, plural, n, ctx, vars...)
|
||||
break
|
||||
}
|
||||
return tr
|
||||
}
|
||||
|
||||
// IsTranslated reports whether a string is translated in given languages.
|
||||
// When the langs argument is omitted, the output of GetLanguages is used.
|
||||
func IsTranslated(str string, langs ...string) bool {
|
||||
return IsTranslatedND(GetDomain(), str, 1, langs...)
|
||||
}
|
||||
|
||||
// IsTranslatedN reports whether a plural string is translated in given languages.
|
||||
// When the langs argument is omitted, the output of GetLanguages is used.
|
||||
func IsTranslatedN(str string, n int, langs ...string) bool {
|
||||
return IsTranslatedND(GetDomain(), str, n, langs...)
|
||||
}
|
||||
|
||||
// IsTranslatedD reports whether a domain string is translated in given languages.
|
||||
// When the langs argument is omitted, the output of GetLanguages is used.
|
||||
func IsTranslatedD(dom, str string, langs ...string) bool {
|
||||
return IsTranslatedND(dom, str, 1, langs...)
|
||||
}
|
||||
|
||||
// IsTranslatedND reports whether a plural domain string is translated in any of given languages.
|
||||
// When the langs argument is omitted, the output of GetLanguages is used.
|
||||
func IsTranslatedND(dom, str string, n int, langs ...string) bool {
|
||||
if len(langs) == 0 {
|
||||
langs = GetLanguages()
|
||||
}
|
||||
|
||||
loadLocales(false)
|
||||
|
||||
globalConfig.RLock()
|
||||
defer globalConfig.RUnlock()
|
||||
|
||||
for _, lang := range langs {
|
||||
lang = SimplifiedLocale(lang)
|
||||
|
||||
for _, supportedLocale := range globalConfig.locales {
|
||||
if lang != supportedLocale.GetActualLanguage(dom) {
|
||||
continue
|
||||
}
|
||||
return supportedLocale.IsTranslatedND(dom, str, n)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsTranslatedC reports whether a context string is translated in given languages.
|
||||
// When the langs argument is omitted, the output of GetLanguages is used.
|
||||
func IsTranslatedC(str, ctx string, langs ...string) bool {
|
||||
return IsTranslatedNDC(GetDomain(), str, 1, ctx, langs...)
|
||||
}
|
||||
|
||||
// IsTranslatedNC reports whether a plural context string is translated in given languages.
|
||||
// When the langs argument is omitted, the output of GetLanguages is used.
|
||||
func IsTranslatedNC(str string, n int, ctx string, langs ...string) bool {
|
||||
return IsTranslatedNDC(GetDomain(), str, n, ctx, langs...)
|
||||
}
|
||||
|
||||
// IsTranslatedDC reports whether a domain context string is translated in given languages.
|
||||
// When the langs argument is omitted, the output of GetLanguages is used.
|
||||
func IsTranslatedDC(dom, str, ctx string, langs ...string) bool {
|
||||
return IsTranslatedNDC(dom, str, 0, ctx, langs...)
|
||||
}
|
||||
|
||||
// IsTranslatedNDC reports whether a plural domain context string is translated in any of given languages.
|
||||
// When the langs argument is omitted, the output of GetLanguages is used.
|
||||
func IsTranslatedNDC(dom, str string, n int, ctx string, langs ...string) bool {
|
||||
if len(langs) == 0 {
|
||||
langs = GetLanguages()
|
||||
}
|
||||
|
||||
loadLocales(false)
|
||||
|
||||
globalConfig.RLock()
|
||||
defer globalConfig.RUnlock()
|
||||
|
||||
for _, lang := range langs {
|
||||
lang = SimplifiedLocale(lang)
|
||||
|
||||
for _, locale := range globalConfig.locales {
|
||||
if lang != locale.GetActualLanguage(dom) {
|
||||
continue
|
||||
}
|
||||
return locale.IsTranslatedNDC(dom, str, n, ctx)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
96
vendor/github.com/leonelquinteros/gotext/helper.go
generated
vendored
Normal file
96
vendor/github.com/leonelquinteros/gotext/helper.go
generated
vendored
Normal file
@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright (c) 2018 DeineAgentur UG https://www.deineagentur.com. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
*/
|
||||
|
||||
package gotext
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var re = regexp.MustCompile(`%\(([a-zA-Z0-9_]+)\)[.0-9]*[svTtbcdoqXxUeEfFgGp]`)
|
||||
|
||||
// SimplifiedLocale simplified locale like " en_US"/"de_DE "/en_US.UTF-8/zh_CN/zh_TW/el_GR@euro/... to en_US, de_DE, zh_CN, el_GR...
|
||||
func SimplifiedLocale(lang string) string {
|
||||
// en_US/en_US.UTF-8/zh_CN/zh_TW/el_GR@euro/...
|
||||
if idx := strings.Index(lang, ":"); idx != -1 {
|
||||
lang = lang[:idx]
|
||||
}
|
||||
if idx := strings.Index(lang, "@"); idx != -1 {
|
||||
lang = lang[:idx]
|
||||
}
|
||||
if idx := strings.Index(lang, "."); idx != -1 {
|
||||
lang = lang[:idx]
|
||||
}
|
||||
return strings.TrimSpace(lang)
|
||||
}
|
||||
|
||||
// FormatString applies text formatting only when needed to parse variables.
|
||||
func FormatString(str string, vars ...interface{}) string {
|
||||
if len(vars) > 0 {
|
||||
return fmt.Sprintf(str, vars...)
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
// Appendf applies text formatting only when needed to parse variables.
|
||||
func Appendf(b []byte, str string, vars ...interface{}) []byte {
|
||||
if len(vars) > 0 {
|
||||
return fmt.Appendf(b, str, vars...)
|
||||
}
|
||||
|
||||
return append(b, str...)
|
||||
}
|
||||
|
||||
// NPrintf support named format
|
||||
// NPrintf("%(name)s is Type %(type)s", map[string]interface{}{"name": "Gotext", "type": "struct"})
|
||||
func NPrintf(format string, params map[string]interface{}) {
|
||||
f, p := parseSprintf(format, params)
|
||||
fmt.Printf(f, p...)
|
||||
}
|
||||
|
||||
// Sprintf support named format
|
||||
//
|
||||
// Sprintf("%(name)s is Type %(type)s", map[string]interface{}{"name": "Gotext", "type": "struct"})
|
||||
func Sprintf(format string, params map[string]interface{}) string {
|
||||
f, p := parseSprintf(format, params)
|
||||
return fmt.Sprintf(f, p...)
|
||||
}
|
||||
|
||||
func parseSprintf(format string, params map[string]interface{}) (string, []interface{}) {
|
||||
f, n := reformatSprintf(format)
|
||||
var p []interface{}
|
||||
for _, v := range n {
|
||||
p = append(p, params[v])
|
||||
}
|
||||
return f, p
|
||||
}
|
||||
|
||||
func reformatSprintf(f string) (string, []string) {
|
||||
m := re.FindAllStringSubmatch(f, -1)
|
||||
i := re.FindAllStringSubmatchIndex(f, -1)
|
||||
|
||||
ord := []string{}
|
||||
for _, v := range m {
|
||||
ord = append(ord, v[1])
|
||||
}
|
||||
|
||||
pair := []int{0}
|
||||
for _, v := range i {
|
||||
pair = append(pair, v[2]-1)
|
||||
pair = append(pair, v[3]+1)
|
||||
}
|
||||
pair = append(pair, len(f))
|
||||
plen := len(pair)
|
||||
|
||||
out := ""
|
||||
for n := 0; n < plen; n += 2 {
|
||||
out += f[pair[n]:pair[n+1]]
|
||||
}
|
||||
|
||||
return out, ord
|
||||
}
|
25
vendor/github.com/leonelquinteros/gotext/introspector.go
generated
vendored
Normal file
25
vendor/github.com/leonelquinteros/gotext/introspector.go
generated
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
package gotext
|
||||
|
||||
// IsTranslatedIntrospector is able to determine whether a given string is translated.
|
||||
// Examples of this introspector are Po and Mo, which are specific to their domain.
|
||||
// Locale holds multiple domains and also implements IsTranslatedDomainIntrospector.
|
||||
type IsTranslatedIntrospector interface {
|
||||
IsTranslated(str string) bool
|
||||
IsTranslatedN(str string, n int) bool
|
||||
IsTranslatedC(str, ctx string) bool
|
||||
IsTranslatedNC(str string, n int, ctx string) bool
|
||||
}
|
||||
|
||||
// IsTranslatedDomainIntrospector is able to determine whether a given string is translated.
|
||||
// Example of this introspector is Locale, which holds multiple domains.
|
||||
// Simpler objects that are domain-specific, like Po or Mo, implement IsTranslatedIntrospector.
|
||||
type IsTranslatedDomainIntrospector interface {
|
||||
IsTranslated(str string) bool
|
||||
IsTranslatedN(str string, n int) bool
|
||||
IsTranslatedD(dom, str string) bool
|
||||
IsTranslatedND(dom, str string, n int) bool
|
||||
IsTranslatedC(str, ctx string) bool
|
||||
IsTranslatedNC(str string, n int, ctx string) bool
|
||||
IsTranslatedDC(dom, str, ctx string) bool
|
||||
IsTranslatedNDC(dom, str string, n int, ctx string) bool
|
||||
}
|
478
vendor/github.com/leonelquinteros/gotext/locale.go
generated
vendored
Normal file
478
vendor/github.com/leonelquinteros/gotext/locale.go
generated
vendored
Normal file
@ -0,0 +1,478 @@
|
||||
/*
|
||||
* Copyright (c) 2018 DeineAgentur UG https://www.deineagentur.com. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
*/
|
||||
|
||||
package gotext
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
)
|
||||
|
||||
/*
|
||||
Locale wraps the entire i18n collection for a single language (locale)
|
||||
It's used by the package functions, but it can also be used independently to handle
|
||||
multiple languages at the same time by working with this object.
|
||||
|
||||
Example:
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create Locale with library path and language code
|
||||
l := gotext.NewLocale("/path/to/i18n/dir", "en_US")
|
||||
|
||||
// Load domain '/path/to/i18n/dir/en_US/LC_MESSAGES/default.{po,mo}'
|
||||
l.AddDomain("default")
|
||||
|
||||
// Translate text from default domain
|
||||
fmt.Println(l.Get("Translate this"))
|
||||
|
||||
// Load different domain ('/path/to/i18n/dir/en_US/LC_MESSAGES/extras.{po,mo}')
|
||||
l.AddDomain("extras")
|
||||
|
||||
// Translate text from domain
|
||||
fmt.Println(l.GetD("extras", "Translate this"))
|
||||
}
|
||||
*/
|
||||
type Locale struct {
|
||||
// Path to locale files.
|
||||
path string
|
||||
|
||||
// Language for this Locale
|
||||
lang string
|
||||
|
||||
// List of available Domains for this locale.
|
||||
Domains map[string]Translator
|
||||
|
||||
// First AddDomain is default Domain
|
||||
defaultDomain string
|
||||
|
||||
// Sync Mutex
|
||||
sync.RWMutex
|
||||
|
||||
// optional fs to use
|
||||
fs fs.FS
|
||||
}
|
||||
|
||||
// NewLocale creates and initializes a new Locale object for a given language.
|
||||
// It receives a path for the i18n .po/.mo files directory (p) and a language code to use (l).
|
||||
func NewLocale(p, l string) *Locale {
|
||||
return &Locale{
|
||||
path: p,
|
||||
lang: SimplifiedLocale(l),
|
||||
Domains: make(map[string]Translator),
|
||||
}
|
||||
}
|
||||
|
||||
// NewLocaleFS returns a Locale working with a fs.FS
|
||||
func NewLocaleFS(l string, filesystem fs.FS) *Locale {
|
||||
loc := NewLocale("", l)
|
||||
loc.fs = filesystem
|
||||
return loc
|
||||
}
|
||||
|
||||
// NewLocaleFSWithPath returns a Locale working with a fs.FS on a p path folder.
|
||||
func NewLocaleFSWithPath(l string, filesystem fs.FS, p string) *Locale {
|
||||
loc := NewLocale("", l)
|
||||
loc.fs = filesystem
|
||||
loc.path = p
|
||||
return loc
|
||||
}
|
||||
|
||||
func (l *Locale) findExt(dom, ext string) string {
|
||||
filename := path.Join(l.path, l.lang, "LC_MESSAGES", dom+"."+ext)
|
||||
if l.fileExists(filename) {
|
||||
return filename
|
||||
}
|
||||
|
||||
if len(l.lang) > 2 {
|
||||
filename = path.Join(l.path, l.lang[:2], "LC_MESSAGES", dom+"."+ext)
|
||||
if l.fileExists(filename) {
|
||||
return filename
|
||||
}
|
||||
}
|
||||
|
||||
filename = path.Join(l.path, l.lang, dom+"."+ext)
|
||||
if l.fileExists(filename) {
|
||||
return filename
|
||||
}
|
||||
|
||||
if len(l.lang) > 2 {
|
||||
filename = path.Join(l.path, l.lang[:2], dom+"."+ext)
|
||||
if l.fileExists(filename) {
|
||||
return filename
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetActualLanguage inspects the filesystem and decides whether to strip
|
||||
// a CC part of the ll_CC locale string.
|
||||
func (l *Locale) GetActualLanguage(dom string) string {
|
||||
extensions := []string{"mo", "po"}
|
||||
var fp string
|
||||
for _, ext := range extensions {
|
||||
// 'll' (or 'll_CC') exists, and it was specified as-is
|
||||
fp = path.Join(l.path, l.lang, "LC_MESSAGES", dom+"."+ext)
|
||||
if l.fileExists(fp) {
|
||||
return l.lang
|
||||
}
|
||||
// 'll' exists, but 'll_CC' was specified
|
||||
if len(l.lang) > 2 {
|
||||
fp = path.Join(l.path, l.lang[:2], "LC_MESSAGES", dom+"."+ext)
|
||||
if l.fileExists(fp) {
|
||||
return l.lang[:2]
|
||||
}
|
||||
}
|
||||
// 'll' (or 'll_CC') exists outside of LC_category, and it was specified as-is
|
||||
fp = path.Join(l.path, l.lang, dom+"."+ext)
|
||||
if l.fileExists(fp) {
|
||||
return l.lang
|
||||
}
|
||||
// 'll' exists outside of LC_category, but 'll_CC' was specified
|
||||
if len(l.lang) > 2 {
|
||||
fp = path.Join(l.path, l.lang[:2], dom+"."+ext)
|
||||
if l.fileExists(fp) {
|
||||
return l.lang[:2]
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (l *Locale) fileExists(filename string) bool {
|
||||
if l.fs != nil {
|
||||
_, err := fs.Stat(l.fs, filename)
|
||||
return err == nil
|
||||
}
|
||||
_, err := os.Stat(filename)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// AddDomain creates a new domain for a given locale object and initializes the Po object.
|
||||
// If the domain exists, it gets reloaded.
|
||||
func (l *Locale) AddDomain(dom string) {
|
||||
var poObj Translator
|
||||
|
||||
file := l.findExt(dom, "po")
|
||||
if file != "" {
|
||||
poObj = NewPoFS(l.fs)
|
||||
// Parse file.
|
||||
poObj.ParseFile(file)
|
||||
} else {
|
||||
file = l.findExt(dom, "mo")
|
||||
if file != "" {
|
||||
poObj = NewMoFS(l.fs)
|
||||
// Parse file.
|
||||
poObj.ParseFile(file)
|
||||
} else {
|
||||
// fallback return if no file found with
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Save new domain
|
||||
l.Lock()
|
||||
|
||||
if l.Domains == nil {
|
||||
l.Domains = make(map[string]Translator)
|
||||
}
|
||||
if l.defaultDomain == "" {
|
||||
l.defaultDomain = dom
|
||||
}
|
||||
l.Domains[dom] = poObj
|
||||
|
||||
// Unlock "Save new domain"
|
||||
l.Unlock()
|
||||
}
|
||||
|
||||
// AddTranslator takes a domain name and a Translator object to make it available in the Locale object.
|
||||
func (l *Locale) AddTranslator(dom string, tr Translator) {
|
||||
l.Lock()
|
||||
|
||||
if l.Domains == nil {
|
||||
l.Domains = make(map[string]Translator)
|
||||
}
|
||||
if l.defaultDomain == "" {
|
||||
l.defaultDomain = dom
|
||||
}
|
||||
l.Domains[dom] = tr
|
||||
|
||||
l.Unlock()
|
||||
}
|
||||
|
||||
// GetDomain is the domain getter for Locale configuration
|
||||
func (l *Locale) GetDomain() string {
|
||||
l.RLock()
|
||||
dom := l.defaultDomain
|
||||
l.RUnlock()
|
||||
return dom
|
||||
}
|
||||
|
||||
// SetDomain sets the name for the domain to be used.
|
||||
func (l *Locale) SetDomain(dom string) {
|
||||
l.Lock()
|
||||
l.defaultDomain = dom
|
||||
l.Unlock()
|
||||
}
|
||||
|
||||
// GetLanguage is the lang getter for Locale configuration
|
||||
func (l *Locale) GetLanguage() string {
|
||||
l.RLock()
|
||||
lang := l.lang
|
||||
l.RUnlock()
|
||||
return lang
|
||||
}
|
||||
|
||||
// Get uses a domain "default" to return the corresponding Translation of a given string.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (l *Locale) Get(str string, vars ...interface{}) string {
|
||||
return l.GetD(l.GetDomain(), str, vars...)
|
||||
}
|
||||
|
||||
// GetN retrieves the (N)th plural form of Translation for the given string in the "default" domain.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (l *Locale) GetN(str, plural string, n int, vars ...interface{}) string {
|
||||
return l.GetND(l.GetDomain(), str, plural, n, vars...)
|
||||
}
|
||||
|
||||
// GetD returns the corresponding Translation in the given domain for the given string.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (l *Locale) GetD(dom, str string, vars ...interface{}) string {
|
||||
// Sync read
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
|
||||
if l.Domains != nil {
|
||||
if _, ok := l.Domains[dom]; ok {
|
||||
if l.Domains[dom] != nil {
|
||||
return l.Domains[dom].Get(str, vars...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return FormatString(str, vars...)
|
||||
}
|
||||
|
||||
// GetND retrieves the (N)th plural form of Translation in the given domain for the given string.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (l *Locale) GetND(dom, str, plural string, n int, vars ...interface{}) string {
|
||||
// Sync read
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
|
||||
if l.Domains != nil {
|
||||
if _, ok := l.Domains[dom]; ok {
|
||||
if l.Domains[dom] != nil {
|
||||
return l.Domains[dom].GetN(str, plural, n, vars...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use western default rule (plural > 1) to handle missing domain default result.
|
||||
if n == 1 {
|
||||
return FormatString(str, vars...)
|
||||
}
|
||||
return FormatString(plural, vars...)
|
||||
}
|
||||
|
||||
// GetC uses a domain "default" to return the corresponding Translation of the given string in the given context.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (l *Locale) GetC(str, ctx string, vars ...interface{}) string {
|
||||
return l.GetDC(l.GetDomain(), str, ctx, vars...)
|
||||
}
|
||||
|
||||
// GetNC retrieves the (N)th plural form of Translation for the given string in the given context in the "default" domain.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (l *Locale) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
|
||||
return l.GetNDC(l.GetDomain(), str, plural, n, ctx, vars...)
|
||||
}
|
||||
|
||||
// GetDC returns the corresponding Translation in the given domain for the given string in the given context.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (l *Locale) GetDC(dom, str, ctx string, vars ...interface{}) string {
|
||||
// Sync read
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
|
||||
if l.Domains != nil {
|
||||
if _, ok := l.Domains[dom]; ok {
|
||||
if l.Domains[dom] != nil {
|
||||
return l.Domains[dom].GetC(str, ctx, vars...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return FormatString(str, vars...)
|
||||
}
|
||||
|
||||
// GetNDC retrieves the (N)th plural form of Translation in the given domain for the given string in the given context.
|
||||
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||
func (l *Locale) GetNDC(dom, str, plural string, n int, ctx string, vars ...interface{}) string {
|
||||
// Sync read
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
|
||||
if l.Domains != nil {
|
||||
if _, ok := l.Domains[dom]; ok {
|
||||
if l.Domains[dom] != nil {
|
||||
return l.Domains[dom].GetNC(str, plural, n, ctx, vars...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use western default rule (plural > 1) to handle missing domain default result.
|
||||
if n == 1 {
|
||||
return FormatString(str, vars...)
|
||||
}
|
||||
return FormatString(plural, vars...)
|
||||
}
|
||||
|
||||
// GetTranslations returns a copy of all translations in all domains of this locale. It does not support contexts.
|
||||
func (l *Locale) GetTranslations() map[string]*Translation {
|
||||
all := make(map[string]*Translation)
|
||||
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
for _, translator := range l.Domains {
|
||||
for msgID, trans := range translator.GetDomain().GetTranslations() {
|
||||
all[msgID] = trans
|
||||
}
|
||||
}
|
||||
|
||||
return all
|
||||
}
|
||||
|
||||
// IsTranslated reports whether a string is translated
|
||||
func (l *Locale) IsTranslated(str string) bool {
|
||||
return l.IsTranslatedND(l.GetDomain(), str, 0)
|
||||
}
|
||||
|
||||
// IsTranslatedN reports whether a plural string is translated
|
||||
func (l *Locale) IsTranslatedN(str string, n int) bool {
|
||||
return l.IsTranslatedND(l.GetDomain(), str, n)
|
||||
}
|
||||
|
||||
// IsTranslatedD reports whether a domain string is translated
|
||||
func (l *Locale) IsTranslatedD(dom, str string) bool {
|
||||
return l.IsTranslatedND(dom, str, 0)
|
||||
}
|
||||
|
||||
// IsTranslatedND reports whether a plural domain string is translated
|
||||
func (l *Locale) IsTranslatedND(dom, str string, n int) bool {
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
|
||||
if l.Domains == nil {
|
||||
return false
|
||||
}
|
||||
translator, ok := l.Domains[dom]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return translator.GetDomain().IsTranslatedN(str, n)
|
||||
}
|
||||
|
||||
// IsTranslatedC reports whether a context string is translated
|
||||
func (l *Locale) IsTranslatedC(str, ctx string) bool {
|
||||
return l.IsTranslatedNDC(l.GetDomain(), str, 0, ctx)
|
||||
}
|
||||
|
||||
// IsTranslatedNC reports whether a plural context string is translated
|
||||
func (l *Locale) IsTranslatedNC(str string, n int, ctx string) bool {
|
||||
return l.IsTranslatedNDC(l.GetDomain(), str, n, ctx)
|
||||
}
|
||||
|
||||
// IsTranslatedDC reports whether a domain context string is translated
|
||||
func (l *Locale) IsTranslatedDC(dom, str, ctx string) bool {
|
||||
return l.IsTranslatedNDC(dom, str, 0, ctx)
|
||||
}
|
||||
|
||||
// IsTranslatedNDC reports whether a plural domain context string is translated
|
||||
func (l *Locale) IsTranslatedNDC(dom string, str string, n int, ctx string) bool {
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
|
||||
if l.Domains == nil {
|
||||
return false
|
||||
}
|
||||
translator, ok := l.Domains[dom]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return translator.GetDomain().IsTranslatedNC(str, n, ctx)
|
||||
}
|
||||
|
||||
// LocaleEncoding is used as intermediary storage to encode Locale objects to Gob.
|
||||
type LocaleEncoding struct {
|
||||
Path string
|
||||
Lang string
|
||||
Domains map[string][]byte
|
||||
DefaultDomain string
|
||||
}
|
||||
|
||||
// MarshalBinary implements encoding BinaryMarshaler interface
|
||||
func (l *Locale) MarshalBinary() ([]byte, error) {
|
||||
obj := new(LocaleEncoding)
|
||||
obj.DefaultDomain = l.defaultDomain
|
||||
obj.Domains = make(map[string][]byte)
|
||||
for k, v := range l.Domains {
|
||||
var err error
|
||||
obj.Domains[k], err = v.MarshalBinary()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
obj.Lang = l.lang
|
||||
obj.Path = l.path
|
||||
|
||||
var buff bytes.Buffer
|
||||
encoder := gob.NewEncoder(&buff)
|
||||
err := encoder.Encode(obj)
|
||||
|
||||
return buff.Bytes(), err
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements encoding BinaryUnmarshaler interface
|
||||
func (l *Locale) UnmarshalBinary(data []byte) error {
|
||||
buff := bytes.NewBuffer(data)
|
||||
obj := new(LocaleEncoding)
|
||||
|
||||
decoder := gob.NewDecoder(buff)
|
||||
err := decoder.Decode(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.defaultDomain = obj.DefaultDomain
|
||||
l.lang = obj.Lang
|
||||
l.path = obj.Path
|
||||
|
||||
// Decode Domains
|
||||
l.Domains = make(map[string]Translator)
|
||||
for k, v := range obj.Domains {
|
||||
var tr TranslatorEncoding
|
||||
buff := bytes.NewBuffer(v)
|
||||
trDecoder := gob.NewDecoder(buff)
|
||||
err := trDecoder.Decode(&tr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.Domains[k] = tr.GetTranslator()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
319
vendor/github.com/leonelquinteros/gotext/mo.go
generated
vendored
Normal file
319
vendor/github.com/leonelquinteros/gotext/mo.go
generated
vendored
Normal file
@ -0,0 +1,319 @@
|
||||
/*
|
||||
* Copyright (c) 2018 DeineAgentur UG https://www.deineagentur.com. All rights reserved.
|
||||
* Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
*/
|
||||
|
||||
package gotext
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
const (
|
||||
// MoMagicLittleEndian encoding
|
||||
MoMagicLittleEndian = 0x950412de
|
||||
// MoMagicBigEndian encoding
|
||||
MoMagicBigEndian = 0xde120495
|
||||
|
||||
// EotSeparator msgctxt and msgid separator
|
||||
EotSeparator = "\x04"
|
||||
// NulSeparator msgid and msgstr separator
|
||||
NulSeparator = "\x00"
|
||||
)
|
||||
|
||||
/*
|
||||
Mo parses the content of any MO file and provides all the Translation functions needed.
|
||||
It's the base object used by all package methods.
|
||||
And it's safe for concurrent use by multiple goroutines by using the sync package for locking.
|
||||
|
||||
Example:
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create mo object
|
||||
mo := gotext.NewMo()
|
||||
|
||||
// Parse .mo file
|
||||
mo.ParseFile("/path/to/po/file/translations.mo")
|
||||
|
||||
// Get Translation
|
||||
fmt.Println(mo.Get("Translate this"))
|
||||
}
|
||||
*/
|
||||
type Mo struct {
|
||||
// these three public members are for backwards compatibility. they are just set to the value in the domain
|
||||
Headers HeaderMap
|
||||
Language string
|
||||
PluralForms string
|
||||
domain *Domain
|
||||
fs fs.FS
|
||||
}
|
||||
|
||||
// NewMo should always be used to instantiate a new Mo object
|
||||
func NewMo() *Mo {
|
||||
mo := new(Mo)
|
||||
mo.domain = NewDomain()
|
||||
|
||||
return mo
|
||||
}
|
||||
|
||||
// NewMoFS works like NewMO but adds an optional fs.FS
|
||||
func NewMoFS(filesystem fs.FS) *Mo {
|
||||
mo := NewMo()
|
||||
mo.fs = filesystem
|
||||
return mo
|
||||
}
|
||||
|
||||
// GetDomain returns the domain object
|
||||
func (mo *Mo) GetDomain() *Domain {
|
||||
return mo.domain
|
||||
}
|
||||
|
||||
// Get returns the translation for the given string
|
||||
func (mo *Mo) Get(str string, vars ...interface{}) string {
|
||||
return mo.domain.Get(str, vars...)
|
||||
}
|
||||
|
||||
// Append a translation string into the domain
|
||||
func (mo *Mo) Append(b []byte, str string, vars ...interface{}) []byte {
|
||||
return mo.domain.Append(b, str, vars...)
|
||||
}
|
||||
|
||||
// GetN returns the translation for the given string and plural form
|
||||
func (mo *Mo) GetN(str, plural string, n int, vars ...interface{}) string {
|
||||
return mo.domain.GetN(str, plural, n, vars...)
|
||||
}
|
||||
|
||||
// AppendN appends a translation string for the given plural form into the domain
|
||||
func (mo *Mo) AppendN(b []byte, str, plural string, n int, vars ...interface{}) []byte {
|
||||
return mo.domain.AppendN(b, str, plural, n, vars...)
|
||||
}
|
||||
|
||||
// GetC returns the translation for the given string and context
|
||||
func (mo *Mo) GetC(str, ctx string, vars ...interface{}) string {
|
||||
return mo.domain.GetC(str, ctx, vars...)
|
||||
}
|
||||
|
||||
// AppendC appends a translation string for the given context into the domain
|
||||
func (mo *Mo) AppendC(b []byte, str, ctx string, vars ...interface{}) []byte {
|
||||
return mo.domain.AppendC(b, str, ctx, vars...)
|
||||
}
|
||||
|
||||
// GetNC returns the translation for the given string, plural form and context
|
||||
func (mo *Mo) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
|
||||
return mo.domain.GetNC(str, plural, n, ctx, vars...)
|
||||
}
|
||||
|
||||
// AppendNC appends a translation string for the given plural form and context into the domain
|
||||
func (mo *Mo) AppendNC(b []byte, str, plural string, n int, ctx string, vars ...interface{}) []byte {
|
||||
return mo.domain.AppendNC(b, str, plural, n, ctx, vars...)
|
||||
}
|
||||
|
||||
// IsTranslated checks if the given string is translated
|
||||
func (mo *Mo) IsTranslated(str string) bool {
|
||||
return mo.domain.IsTranslated(str)
|
||||
}
|
||||
|
||||
// IsTranslatedN checks if the given string is translated with plural form
|
||||
func (mo *Mo) IsTranslatedN(str string, n int) bool {
|
||||
return mo.domain.IsTranslatedN(str, n)
|
||||
}
|
||||
|
||||
// IsTranslatedC checks if the given string is translated with context
|
||||
func (mo *Mo) IsTranslatedC(str, ctx string) bool {
|
||||
return mo.domain.IsTranslatedC(str, ctx)
|
||||
}
|
||||
|
||||
// IsTranslatedNC checks if the given string is translated with plural form and context
|
||||
func (mo *Mo) IsTranslatedNC(str string, n int, ctx string) bool {
|
||||
return mo.domain.IsTranslatedNC(str, n, ctx)
|
||||
}
|
||||
|
||||
// MarshalBinary marshals the Mo object into a binary format
|
||||
func (mo *Mo) MarshalBinary() ([]byte, error) {
|
||||
return mo.domain.MarshalBinary()
|
||||
}
|
||||
|
||||
// UnmarshalBinary unmarshals the Mo object from a binary format
|
||||
func (mo *Mo) UnmarshalBinary(data []byte) error {
|
||||
return mo.domain.UnmarshalBinary(data)
|
||||
}
|
||||
|
||||
// ParseFile loads the translations specified in the provided file, in the GNU gettext .mo format
|
||||
func (mo *Mo) ParseFile(f string) {
|
||||
data, err := getFileData(f, mo.fs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
mo.Parse(data)
|
||||
}
|
||||
|
||||
// Parse loads the translations specified in the provided byte slice, in the GNU gettext .mo format
|
||||
func (mo *Mo) Parse(buf []byte) {
|
||||
// Lock while parsing
|
||||
mo.domain.trMutex.Lock()
|
||||
mo.domain.pluralMutex.Lock()
|
||||
defer mo.domain.trMutex.Unlock()
|
||||
defer mo.domain.pluralMutex.Unlock()
|
||||
|
||||
r := bytes.NewReader(buf)
|
||||
|
||||
var magicNumber uint32
|
||||
if err := binary.Read(r, binary.LittleEndian, &magicNumber); err != nil {
|
||||
return
|
||||
// return fmt.Errorf("gettext: %v", err)
|
||||
}
|
||||
var bo binary.ByteOrder
|
||||
switch magicNumber {
|
||||
case MoMagicLittleEndian:
|
||||
bo = binary.LittleEndian
|
||||
case MoMagicBigEndian:
|
||||
bo = binary.BigEndian
|
||||
default:
|
||||
return
|
||||
// return fmt.Errorf("gettext: %v", "invalid magic number")
|
||||
}
|
||||
|
||||
var header struct {
|
||||
MajorVersion uint16
|
||||
MinorVersion uint16
|
||||
MsgIDCount uint32
|
||||
MsgIDOffset uint32
|
||||
MsgStrOffset uint32
|
||||
HashSize uint32
|
||||
HashOffset uint32
|
||||
}
|
||||
if err := binary.Read(r, bo, &header); err != nil {
|
||||
return
|
||||
// return fmt.Errorf("gettext: %v", err)
|
||||
}
|
||||
if v := header.MajorVersion; v != 0 && v != 1 {
|
||||
return
|
||||
// return fmt.Errorf("gettext: %v", "invalid version number")
|
||||
}
|
||||
if v := header.MinorVersion; v != 0 && v != 1 {
|
||||
return
|
||||
// return fmt.Errorf("gettext: %v", "invalid version number")
|
||||
}
|
||||
|
||||
msgIDStart := make([]uint32, header.MsgIDCount)
|
||||
msgIDLen := make([]uint32, header.MsgIDCount)
|
||||
if _, err := r.Seek(int64(header.MsgIDOffset), 0); err != nil {
|
||||
return
|
||||
// return fmt.Errorf("gettext: %v", err)
|
||||
}
|
||||
for i := 0; i < int(header.MsgIDCount); i++ {
|
||||
if err := binary.Read(r, bo, &msgIDLen[i]); err != nil {
|
||||
return
|
||||
// return fmt.Errorf("gettext: %v", err)
|
||||
}
|
||||
if err := binary.Read(r, bo, &msgIDStart[i]); err != nil {
|
||||
return
|
||||
// return fmt.Errorf("gettext: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
msgStrStart := make([]int32, header.MsgIDCount)
|
||||
msgStrLen := make([]int32, header.MsgIDCount)
|
||||
if _, err := r.Seek(int64(header.MsgStrOffset), 0); err != nil {
|
||||
return
|
||||
// return fmt.Errorf("gettext: %v", err)
|
||||
}
|
||||
for i := 0; i < int(header.MsgIDCount); i++ {
|
||||
if err := binary.Read(r, bo, &msgStrLen[i]); err != nil {
|
||||
return
|
||||
// return fmt.Errorf("gettext: %v", err)
|
||||
}
|
||||
if err := binary.Read(r, bo, &msgStrStart[i]); err != nil {
|
||||
return
|
||||
// return fmt.Errorf("gettext: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < int(header.MsgIDCount); i++ {
|
||||
if _, err := r.Seek(int64(msgIDStart[i]), 0); err != nil {
|
||||
return
|
||||
// return fmt.Errorf("gettext: %v", err)
|
||||
}
|
||||
msgIDData := make([]byte, msgIDLen[i])
|
||||
if _, err := r.Read(msgIDData); err != nil {
|
||||
return
|
||||
// return fmt.Errorf("gettext: %v", err)
|
||||
}
|
||||
|
||||
if _, err := r.Seek(int64(msgStrStart[i]), 0); err != nil {
|
||||
return
|
||||
// return fmt.Errorf("gettext: %v", err)
|
||||
}
|
||||
msgStrData := make([]byte, msgStrLen[i])
|
||||
if _, err := r.Read(msgStrData); err != nil {
|
||||
return
|
||||
// return fmt.Errorf("gettext: %v", err)
|
||||
}
|
||||
|
||||
if len(msgIDData) == 0 {
|
||||
mo.addTranslation(msgIDData, msgStrData)
|
||||
} else {
|
||||
mo.addTranslation(msgIDData, msgStrData)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse headers
|
||||
mo.domain.parseHeaders()
|
||||
|
||||
// set values on this struct
|
||||
// this is for backwards compatibility
|
||||
mo.Language = mo.domain.Language
|
||||
mo.PluralForms = mo.domain.PluralForms
|
||||
mo.Headers = mo.domain.Headers
|
||||
}
|
||||
|
||||
func (mo *Mo) addTranslation(msgid, msgstr []byte) {
|
||||
translation := NewTranslation()
|
||||
var msgctxt []byte
|
||||
var msgidPlural []byte
|
||||
|
||||
d := bytes.Split(msgid, []byte(EotSeparator))
|
||||
if len(d) == 1 {
|
||||
msgid = d[0]
|
||||
} else {
|
||||
msgid, msgctxt = d[1], d[0]
|
||||
}
|
||||
|
||||
dd := bytes.Split(msgid, []byte(NulSeparator))
|
||||
if len(dd) > 1 {
|
||||
msgid = dd[0]
|
||||
dd = dd[1:]
|
||||
}
|
||||
|
||||
translation.ID = string(msgid)
|
||||
|
||||
msgidPlural = bytes.Join(dd, []byte(NulSeparator))
|
||||
if len(msgidPlural) > 0 {
|
||||
translation.PluralID = string(msgidPlural)
|
||||
}
|
||||
|
||||
ddd := bytes.Split(msgstr, []byte(NulSeparator))
|
||||
if len(ddd) > 0 {
|
||||
for i, s := range ddd {
|
||||
translation.Trs[i] = string(s)
|
||||
}
|
||||
}
|
||||
|
||||
if len(msgctxt) > 0 {
|
||||
// With context...
|
||||
if _, ok := mo.domain.contextTranslations[string(msgctxt)]; !ok {
|
||||
mo.domain.contextTranslations[string(msgctxt)] = make(map[string]*Translation)
|
||||
}
|
||||
mo.domain.contextTranslations[string(msgctxt)][translation.ID] = translation
|
||||
} else {
|
||||
mo.domain.translations[translation.ID] = translation
|
||||
}
|
||||
}
|
433
vendor/github.com/leonelquinteros/gotext/plurals/compiler.go
generated
vendored
Normal file
433
vendor/github.com/leonelquinteros/gotext/plurals/compiler.go
generated
vendored
Normal file
@ -0,0 +1,433 @@
|
||||
// Original work Copyright (c) 2016 Jonas Obrist (https://github.com/ojii/gettext.go)
|
||||
// Modified work Copyright (c) 2018 DeineAgentur UG https://www.deineagentur.com
|
||||
// Modified work Copyright (c) 2018-present gotext maintainers (https://github.com/leonelquinteros/gotext)
|
||||
//
|
||||
// Licensed under the 3-Clause BSD License. See LICENSE in the project root for license information.
|
||||
|
||||
/*
|
||||
Package plurals is the pluralform compiler to get the correct translation id of the plural string
|
||||
*/
|
||||
package plurals
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type match struct {
|
||||
openPos int
|
||||
closePos int
|
||||
}
|
||||
|
||||
var pat = regexp.MustCompile(`(\?|:|\|\||&&|==|!=|>=|>|<=|<|%|\d+|n)`)
|
||||
|
||||
type testToken interface {
|
||||
compile(tokens []string) (test test, err error)
|
||||
}
|
||||
|
||||
type cmpTestBuilder func(val uint32, flipped bool) test
|
||||
type logicTestBuild func(left test, right test) test
|
||||
|
||||
var ternaryToken ternaryStruct
|
||||
|
||||
type ternaryStruct struct{}
|
||||
|
||||
func (ternaryStruct) compile(tokens []string) (expr Expression, err error) {
|
||||
main, err := splitTokens(tokens, "?")
|
||||
if err != nil {
|
||||
return expr, err
|
||||
}
|
||||
test, err := compileTest(strings.Join(main.Left, ""))
|
||||
if err != nil {
|
||||
return expr, err
|
||||
}
|
||||
actions, err := splitTokens(main.Right, ":")
|
||||
if err != nil {
|
||||
return expr, err
|
||||
}
|
||||
trueAction, err := compileExpression(strings.Join(actions.Left, ""))
|
||||
if err != nil {
|
||||
return expr, err
|
||||
}
|
||||
falseAction, err := compileExpression(strings.Join(actions.Right, ""))
|
||||
if err != nil {
|
||||
return expr, nil
|
||||
}
|
||||
return ternary{
|
||||
test: test,
|
||||
trueExpr: trueAction,
|
||||
falseExpr: falseAction,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var constToken constValStruct
|
||||
|
||||
type constValStruct struct{}
|
||||
|
||||
func (constValStruct) compile(tokens []string) (expr Expression, err error) {
|
||||
if len(tokens) == 0 {
|
||||
return expr, errors.New("got nothing instead of constant")
|
||||
}
|
||||
if len(tokens) != 1 {
|
||||
return expr, fmt.Errorf("invalid constant: %s", strings.Join(tokens, ""))
|
||||
}
|
||||
i, err := strconv.Atoi(tokens[0])
|
||||
if err != nil {
|
||||
return expr, err
|
||||
}
|
||||
return constValue{value: i}, nil
|
||||
}
|
||||
|
||||
func compileLogicTest(tokens []string, sep string, builder logicTestBuild) (test test, err error) {
|
||||
split, err := splitTokens(tokens, sep)
|
||||
if err != nil {
|
||||
return test, err
|
||||
}
|
||||
left, err := compileTest(strings.Join(split.Left, ""))
|
||||
if err != nil {
|
||||
return test, err
|
||||
}
|
||||
right, err := compileTest(strings.Join(split.Right, ""))
|
||||
if err != nil {
|
||||
return test, err
|
||||
}
|
||||
return builder(left, right), nil
|
||||
}
|
||||
|
||||
var orToken orStruct
|
||||
|
||||
type orStruct struct{}
|
||||
|
||||
func (orStruct) compile(tokens []string) (test test, err error) {
|
||||
return compileLogicTest(tokens, "||", buildOr)
|
||||
}
|
||||
func buildOr(left test, right test) test {
|
||||
return or{left: left, right: right}
|
||||
}
|
||||
|
||||
var andToken andStruct
|
||||
|
||||
type andStruct struct{}
|
||||
|
||||
func (andStruct) compile(tokens []string) (test test, err error) {
|
||||
return compileLogicTest(tokens, "&&", buildAnd)
|
||||
}
|
||||
func buildAnd(left test, right test) test {
|
||||
return and{left: left, right: right}
|
||||
}
|
||||
|
||||
func compileMod(tokens []string) (math math, err error) {
|
||||
split, err := splitTokens(tokens, "%")
|
||||
if err != nil {
|
||||
return math, err
|
||||
}
|
||||
if len(split.Left) != 1 || split.Left[0] != "n" {
|
||||
return math, errors.New("modulus operation requires 'n' as left operand")
|
||||
}
|
||||
if len(split.Right) != 1 {
|
||||
return math, errors.New("modulus operation requires simple integer as right operand")
|
||||
}
|
||||
i, err := parseUint32(split.Right[0])
|
||||
if err != nil {
|
||||
return math, err
|
||||
}
|
||||
return mod{value: uint32(i)}, nil
|
||||
}
|
||||
|
||||
func subPipe(modTokens []string, actionTokens []string, builder cmpTestBuilder, flipped bool) (test test, err error) {
|
||||
modifier, err := compileMod(modTokens)
|
||||
if err != nil {
|
||||
return test, err
|
||||
}
|
||||
if len(actionTokens) != 1 {
|
||||
return test, errors.New("can only get modulus of integer")
|
||||
}
|
||||
i, err := parseUint32(actionTokens[0])
|
||||
if err != nil {
|
||||
return test, err
|
||||
}
|
||||
action := builder(uint32(i), flipped)
|
||||
return pipe{
|
||||
modifier: modifier,
|
||||
action: action,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func compileEquality(tokens []string, sep string, builder cmpTestBuilder) (test test, err error) {
|
||||
split, err := splitTokens(tokens, sep)
|
||||
if err != nil {
|
||||
return test, err
|
||||
}
|
||||
if len(split.Left) == 1 && split.Left[0] == "n" {
|
||||
if len(split.Right) != 1 {
|
||||
return test, errors.New("test can only compare n to integers")
|
||||
}
|
||||
i, err := parseUint32(split.Right[0])
|
||||
if err != nil {
|
||||
return test, err
|
||||
}
|
||||
return builder(i, false), nil
|
||||
} else if len(split.Right) == 1 && split.Right[0] == "n" {
|
||||
if len(split.Left) != 1 {
|
||||
return test, errors.New("test can only compare n to integers")
|
||||
}
|
||||
i, err := parseUint32(split.Left[0])
|
||||
if err != nil {
|
||||
return test, err
|
||||
}
|
||||
return builder(i, true), nil
|
||||
} else if contains(split.Left, "n") && contains(split.Left, "%") {
|
||||
return subPipe(split.Left, split.Right, builder, false)
|
||||
}
|
||||
return test, errors.New("equality test must have 'n' as one of the two tests")
|
||||
|
||||
}
|
||||
|
||||
var eqToken eqStruct
|
||||
|
||||
type eqStruct struct{}
|
||||
|
||||
func (eqStruct) compile(tokens []string) (test test, err error) {
|
||||
return compileEquality(tokens, "==", buildEq)
|
||||
}
|
||||
func buildEq(val uint32, flipped bool) test {
|
||||
return equal{value: val}
|
||||
}
|
||||
|
||||
var neqToken neqStruct
|
||||
|
||||
type neqStruct struct{}
|
||||
|
||||
func (neqStruct) compile(tokens []string) (test test, err error) {
|
||||
return compileEquality(tokens, "!=", buildNeq)
|
||||
}
|
||||
func buildNeq(val uint32, flipped bool) test {
|
||||
return notequal{value: val}
|
||||
}
|
||||
|
||||
var gtToken gtStruct
|
||||
|
||||
type gtStruct struct{}
|
||||
|
||||
func (gtStruct) compile(tokens []string) (test test, err error) {
|
||||
return compileEquality(tokens, ">", buildGt)
|
||||
}
|
||||
func buildGt(val uint32, flipped bool) test {
|
||||
return gt{value: val, flipped: flipped}
|
||||
}
|
||||
|
||||
var gteToken gteStruct
|
||||
|
||||
type gteStruct struct{}
|
||||
|
||||
func (gteStruct) compile(tokens []string) (test test, err error) {
|
||||
return compileEquality(tokens, ">=", buildGte)
|
||||
}
|
||||
func buildGte(val uint32, flipped bool) test {
|
||||
return gte{value: val, flipped: flipped}
|
||||
}
|
||||
|
||||
var ltToken ltStruct
|
||||
|
||||
type ltStruct struct{}
|
||||
|
||||
func (ltStruct) compile(tokens []string) (test test, err error) {
|
||||
return compileEquality(tokens, "<", buildLt)
|
||||
}
|
||||
func buildLt(val uint32, flipped bool) test {
|
||||
return lt{value: val, flipped: flipped}
|
||||
}
|
||||
|
||||
var lteToken lteStruct
|
||||
|
||||
type lteStruct struct{}
|
||||
|
||||
func (lteStruct) compile(tokens []string) (test test, err error) {
|
||||
return compileEquality(tokens, "<=", buildLte)
|
||||
}
|
||||
func buildLte(val uint32, flipped bool) test {
|
||||
return lte{value: val, flipped: flipped}
|
||||
}
|
||||
|
||||
type testTokenDef struct {
|
||||
op string
|
||||
token testToken
|
||||
}
|
||||
|
||||
var precedence = []testTokenDef{
|
||||
{op: "||", token: orToken},
|
||||
{op: "&&", token: andToken},
|
||||
{op: "==", token: eqToken},
|
||||
{op: "!=", token: neqToken},
|
||||
{op: ">=", token: gteToken},
|
||||
{op: ">", token: gtToken},
|
||||
{op: "<=", token: lteToken},
|
||||
{op: "<", token: ltToken},
|
||||
}
|
||||
|
||||
type splitted struct {
|
||||
Left []string
|
||||
Right []string
|
||||
}
|
||||
|
||||
// Find index of token in list of tokens
|
||||
func index(tokens []string, sep string) int {
|
||||
for index, token := range tokens {
|
||||
if token == sep {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// Split a list of tokens by a token into a splitted struct holding the tokens
|
||||
// before and after the token to be split by.
|
||||
func splitTokens(tokens []string, sep string) (s splitted, err error) {
|
||||
index := index(tokens, sep)
|
||||
if index == -1 {
|
||||
return s, fmt.Errorf("'%s' not found in ['%s']", sep, strings.Join(tokens, "','"))
|
||||
}
|
||||
return splitted{
|
||||
Left: tokens[:index],
|
||||
Right: tokens[index+1:],
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Scan a string for parenthesis
|
||||
func scan(s string) <-chan match {
|
||||
ch := make(chan match)
|
||||
go func() {
|
||||
depth := 0
|
||||
opener := 0
|
||||
for index, char := range s {
|
||||
switch char {
|
||||
case '(':
|
||||
if depth == 0 {
|
||||
opener = index
|
||||
}
|
||||
depth++
|
||||
case ')':
|
||||
depth--
|
||||
if depth == 0 {
|
||||
ch <- match{
|
||||
openPos: opener,
|
||||
closePos: index + 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
close(ch)
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
// Split the string into tokens
|
||||
func split(s string) <-chan string {
|
||||
ch := make(chan string)
|
||||
go func() {
|
||||
s = strings.ReplaceAll(s, " ", "")
|
||||
if !strings.Contains(s, "(") {
|
||||
ch <- s
|
||||
} else {
|
||||
last := 0
|
||||
end := len(s)
|
||||
for info := range scan(s) {
|
||||
if last != info.openPos {
|
||||
ch <- s[last:info.openPos]
|
||||
}
|
||||
ch <- s[info.openPos:info.closePos]
|
||||
last = info.closePos
|
||||
}
|
||||
if last != end {
|
||||
ch <- s[last:]
|
||||
}
|
||||
}
|
||||
close(ch)
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
// Tokenizes a string into a list of strings, tokens grouped by parenthesis are
|
||||
// not split! If the string starts with ( and ends in ), those are stripped.
|
||||
func tokenize(s string) []string {
|
||||
/*
|
||||
TODO: Properly detect if the string starts with a ( and ends with a )
|
||||
and that those two form a matching pair.
|
||||
|
||||
Eg: (foo) -> true; (foo)(bar) -> false;
|
||||
*/
|
||||
if len(s) == 0 {
|
||||
return []string{}
|
||||
}
|
||||
if s[0] == '(' && s[len(s)-1] == ')' {
|
||||
s = s[1 : len(s)-1]
|
||||
}
|
||||
ret := []string{}
|
||||
for chunk := range split(s) {
|
||||
if len(chunk) != 0 {
|
||||
if chunk[0] == '(' && chunk[len(chunk)-1] == ')' {
|
||||
ret = append(ret, chunk)
|
||||
} else {
|
||||
for _, token := range pat.FindAllStringSubmatch(chunk, -1) {
|
||||
ret = append(ret, token[0])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("Empty chunk in string '%s'\n", s)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// Compile a string containing a plural form expression to a Expression object.
|
||||
func Compile(s string) (expr Expression, err error) {
|
||||
if s == "0" {
|
||||
return constValue{value: 0}, nil
|
||||
}
|
||||
if !strings.Contains(s, "?") {
|
||||
s += "?1:0"
|
||||
}
|
||||
return compileExpression(s)
|
||||
}
|
||||
|
||||
// Check if a token is in a slice of strings
|
||||
func contains(haystack []string, needle string) bool {
|
||||
for _, s := range haystack {
|
||||
if s == needle {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Compiles an expression (ternary or constant)
|
||||
func compileExpression(s string) (expr Expression, err error) {
|
||||
tokens := tokenize(s)
|
||||
if contains(tokens, "?") {
|
||||
return ternaryToken.compile(tokens)
|
||||
}
|
||||
return constToken.compile(tokens)
|
||||
}
|
||||
|
||||
// Compiles a test (comparison)
|
||||
func compileTest(s string) (test test, err error) {
|
||||
tokens := tokenize(s)
|
||||
for _, tokenDef := range precedence {
|
||||
if contains(tokens, tokenDef.op) {
|
||||
return tokenDef.token.compile(tokens)
|
||||
}
|
||||
}
|
||||
return test, errors.New("cannot compile")
|
||||
}
|
||||
|
||||
func parseUint32(s string) (ui uint32, err error) {
|
||||
i, err := strconv.ParseUint(s, 10, 32)
|
||||
if err != nil {
|
||||
return ui, err
|
||||
}
|
||||
return uint32(i), nil
|
||||
}
|
44
vendor/github.com/leonelquinteros/gotext/plurals/expression.go
generated
vendored
Normal file
44
vendor/github.com/leonelquinteros/gotext/plurals/expression.go
generated
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
// Original work Copyright (c) 2016 Jonas Obrist (https://github.com/ojii/gettext.go)
|
||||
// Modified work Copyright (c) 2018 DeineAgentur UG https://www.deineagentur.com
|
||||
// Modified work Copyright (c) 2018-present gotext maintainers (https://github.com/leonelquinteros/gotext)
|
||||
//
|
||||
// Licensed under the 3-Clause BSD License. See LICENSE in the project root for license information.
|
||||
|
||||
package plurals
|
||||
|
||||
// Expression is a plurals expression. Eval evaluates the expression for
|
||||
// a given n value. Use plurals.Compile to generate Expression instances.
|
||||
type Expression interface {
|
||||
Eval(n uint32) int
|
||||
}
|
||||
|
||||
type constValue struct {
|
||||
value int
|
||||
}
|
||||
|
||||
func (c constValue) Eval(n uint32) int {
|
||||
return c.value
|
||||
}
|
||||
|
||||
type test interface {
|
||||
test(n uint32) bool
|
||||
}
|
||||
|
||||
type ternary struct {
|
||||
test test
|
||||
trueExpr Expression
|
||||
falseExpr Expression
|
||||
}
|
||||
|
||||
func (t ternary) Eval(n uint32) int {
|
||||
if t.test.test(n) {
|
||||
if t.trueExpr == nil {
|
||||
return -1
|
||||
}
|
||||
return t.trueExpr.Eval(n)
|
||||
}
|
||||
if t.falseExpr == nil {
|
||||
return -1
|
||||
}
|
||||
return t.falseExpr.Eval(n)
|
||||
}
|
41
vendor/github.com/leonelquinteros/gotext/plurals/genfixture.py
generated
vendored
Normal file
41
vendor/github.com/leonelquinteros/gotext/plurals/genfixture.py
generated
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
#!/usr/bin/python
|
||||
#
|
||||
# Copyright (c) 2016 Jonas Obrist (https://github.com/ojii/gettext.go)
|
||||
#
|
||||
# Licensed under the 3-Clause BSD License. See LICENSE in the project root for license information.
|
||||
|
||||
import json
|
||||
from gettext import c2py
|
||||
|
||||
|
||||
PLURAL_FORMS = [
|
||||
"0",
|
||||
"n!=1",
|
||||
"n>1",
|
||||
"n%10==1&&n%100!=11?0:n!=0?1:2",
|
||||
"n==1?0:n==2?1:2",
|
||||
"n==1?0:(n==0||(n%100>0&&n%100<20))?1:2",
|
||||
"n%10==1&&n%100!=11?0:n%10>=2&&(n%100<10||n%100>=20)?1:2",
|
||||
"n%10==1&&n%100!=11?0:n%10>=2&&n%10<=4&&(n%100<10||n%100>=20)?1:2",
|
||||
"(n==1)?0:(n>=2&&n<=4)?1:2",
|
||||
"n==1?0:n%10>=2&&n%10<=4&&(n%100<10||n%100>=20)?1:2",
|
||||
"n%100==1?0:n%100==2?1:n%100==3||n%100==4?2:3",
|
||||
"n==0?0:n==1?1:n==2?2:n%100>=3&&n%100<=10?3:n%100>=11?4:5",
|
||||
]
|
||||
|
||||
NUM = 1000
|
||||
|
||||
|
||||
def gen():
|
||||
tests = []
|
||||
for plural_form in PLURAL_FORMS:
|
||||
expr = c2py(plural_form)
|
||||
tests.append({
|
||||
'pluralform': plural_form,
|
||||
'fixture': [expr(n) for n in range(NUM + 1)]
|
||||
})
|
||||
return json.dumps(tests)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(gen())
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user