Compare commits
31 Commits
fix/613
...
fixBrokenR
| Author | SHA1 | Date | |
|---|---|---|---|
| a51a3b57f6 | |||
| 683a3bbf3d | |||
| ae9e49e1b5 | |||
| 5b3929d885 | |||
| c41df874d1 | |||
|
b721adbf9c
|
|||
| 42f9e6d458 | |||
| 9e7bc31d4d | |||
| b79c4f33b6 | |||
| cc87d5b3da | |||
| 8b5e3f3c78 | |||
|
db7c4042d0
|
|||
|
ed1a66dc5f
|
|||
|
bb93e4266a
|
|||
|
a2cc70b2f5
|
|||
|
ce1aa3d870
|
|||
|
d75700c8a9
|
|||
|
0ccc4aae72
|
|||
|
ec22d5d51d
|
|||
|
ab42584d05
|
|||
| 40eb6e9a18 | |||
| 35eb9d4a89 | |||
|
08cc63d523
|
|||
| 797b8d899b | |||
|
fb786306b5
|
|||
| c3a2048eba | |||
| 1bdc11ba62 | |||
| cc8703310c | |||
|
fcd5bd863d
|
|||
|
e6af2da9dd
|
|||
| 4b688825e0 |
@ -260,6 +260,7 @@ checkout as-is. Recipe commit hashes are also supported as values for
|
|||||||
app.Name,
|
app.Name,
|
||||||
app.Server,
|
app.Server,
|
||||||
internal.DontWaitConverge,
|
internal.DontWaitConverge,
|
||||||
|
internal.NoInput,
|
||||||
f,
|
f,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
@ -280,13 +281,21 @@ checkout as-is. Recipe commit hashes are also supported as values for
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getLatestVersionOrCommit(app appPkg.App) (string, error) {
|
func getLatestVersionOrCommit(app appPkg.App) (string, error) {
|
||||||
versions, err := app.Recipe.Tags()
|
recipeVersions, warnings, err := app.Recipe.GetRecipeVersions()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(versions) > 0 && !internal.Chaos {
|
for _, warning := range warnings {
|
||||||
return versions[len(versions)-1], nil
|
log.Warn(warning)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(recipeVersions) > 0 && !internal.Chaos {
|
||||||
|
latest := recipeVersions[len(recipeVersions)-1]
|
||||||
|
for tag := range latest {
|
||||||
|
log.Debug(i18n.G("selected latest recipe version: %s (from %d available versions)", tag, len(recipeVersions)))
|
||||||
|
return tag, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
head, err := app.Recipe.Head()
|
head, err := app.Recipe.Head()
|
||||||
|
|||||||
@ -98,10 +98,14 @@ var AppNewCommand = &cobra.Command{
|
|||||||
var recipeVersions recipePkg.RecipeVersions
|
var recipeVersions recipePkg.RecipeVersions
|
||||||
if recipeVersion == "" {
|
if recipeVersion == "" {
|
||||||
var err error
|
var err error
|
||||||
recipeVersions, _, err = recipe.GetRecipeVersions()
|
var warnings []string
|
||||||
|
recipeVersions, warnings, err = recipe.GetRecipeVersions()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
for _, warning := range warnings {
|
||||||
|
log.Warn(warning)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(recipeVersions) > 0 {
|
if len(recipeVersions) > 0 {
|
||||||
@ -110,6 +114,8 @@ var AppNewCommand = &cobra.Command{
|
|||||||
recipeVersion = tag
|
recipeVersion = tag
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debug(i18n.G("selected recipe version: %s (from %d available versions)", recipeVersion, len(recipeVersions)))
|
||||||
|
|
||||||
if _, err := recipe.EnsureVersion(recipeVersion); err != nil {
|
if _, err := recipe.EnsureVersion(recipeVersion); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -128,6 +128,7 @@ Pass "--all-services/-a" to restart all services.`),
|
|||||||
AppName: app.Name,
|
AppName: app.Name,
|
||||||
ServerName: app.Server,
|
ServerName: app.Server,
|
||||||
Filters: f,
|
Filters: f,
|
||||||
|
NoInput: internal.NoInput,
|
||||||
NoLog: true,
|
NoLog: true,
|
||||||
Quiet: true,
|
Quiet: true,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -246,6 +246,7 @@ beforehand. See "abra app backup" for more.`),
|
|||||||
stackName,
|
stackName,
|
||||||
app.Server,
|
app.Server,
|
||||||
internal.DontWaitConverge,
|
internal.DontWaitConverge,
|
||||||
|
internal.NoInput,
|
||||||
f,
|
f,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
|||||||
@ -282,6 +282,7 @@ beforehand. See "abra app backup" for more.`),
|
|||||||
stackName,
|
stackName,
|
||||||
app.Server,
|
app.Server,
|
||||||
internal.DontWaitConverge,
|
internal.DontWaitConverge,
|
||||||
|
internal.NoInput,
|
||||||
f,
|
f,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
|||||||
@ -64,7 +64,7 @@ func DeployOverview(
|
|||||||
server = "local"
|
server = "local"
|
||||||
}
|
}
|
||||||
|
|
||||||
domain := app.Domain
|
domain := fmt.Sprintf("https://%s", app.Domain)
|
||||||
if domain == "" {
|
if domain == "" {
|
||||||
domain = config.MISSING_DEFAULT
|
domain = config.MISSING_DEFAULT
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package recipe
|
package recipe
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -19,6 +20,9 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Errors
|
||||||
|
var emptyVersionsInCatalogue = errors.New(i18n.G("catalogue versions list is unexpectedly empty"))
|
||||||
|
|
||||||
// translators: `abra recipe reset` aliases. use a comma separated list of
|
// translators: `abra recipe reset` aliases. use a comma separated list of
|
||||||
// aliases with no spaces in between
|
// aliases with no spaces in between
|
||||||
var recipeSyncAliases = i18n.G("s")
|
var recipeSyncAliases = i18n.G("s")
|
||||||
@ -121,20 +125,18 @@ likely to change.
|
|||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
versions, err := recipePkg.GetRecipeCatalogueVersions(recipe.Name, catl)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
changesTable, err := formatter.CreateTable()
|
changesTable, err := formatter.CreateTable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
latestRelease := tags[len(tags)-1]
|
latestRelease := tags[len(tags)-1]
|
||||||
|
latestRecipeVersion, err := getLatestVersion(recipe, catl)
|
||||||
|
if err != nil && err != emptyVersionsInCatalogue {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
changesTable.Headers(i18n.G("SERVICE"), latestRelease, i18n.G("PROPOSED CHANGES"))
|
changesTable.Headers(i18n.G("SERVICE"), latestRelease, i18n.G("PROPOSED CHANGES"))
|
||||||
|
|
||||||
latestRecipeVersion := versions[len(versions)-1]
|
|
||||||
allRecipeVersions := catl[recipe.Name].Versions
|
allRecipeVersions := catl[recipe.Name].Versions
|
||||||
for _, recipeVersion := range allRecipeVersions {
|
for _, recipeVersion := range allRecipeVersions {
|
||||||
if serviceVersions, ok := recipeVersion[latestRecipeVersion]; ok {
|
if serviceVersions, ok := recipeVersion[latestRecipeVersion]; ok {
|
||||||
@ -298,3 +300,14 @@ func init() {
|
|||||||
i18n.G("increase the patch part of the version"),
|
i18n.G("increase the patch part of the version"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getLatestVersion(recipe recipePkg.Recipe, catl recipePkg.RecipeCatalogue) (string, error) {
|
||||||
|
versions, err := recipePkg.GetRecipeCatalogueVersions(recipe.Name, catl)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(versions) > 0 {
|
||||||
|
return versions[len(versions)-1], nil
|
||||||
|
}
|
||||||
|
return "", emptyVersionsInCatalogue
|
||||||
|
}
|
||||||
|
|||||||
33
cli/recipe/sync_test.go
Normal file
33
cli/recipe/sync_test.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package recipe
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
recipePkg "coopcloud.tech/abra/pkg/recipe"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetLatestVersionReturnsErrorWhenVersionsIsEmpty(t *testing.T) {
|
||||||
|
recipe := recipePkg.Recipe{}
|
||||||
|
catalogue := recipePkg.RecipeCatalogue{}
|
||||||
|
_, err := getLatestVersion(recipe, catalogue)
|
||||||
|
assert.Equal(t, err, emptyVersionsInCatalogue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetLatestVersionReturnsLastVersion(t *testing.T) {
|
||||||
|
recipe := recipePkg.Recipe{
|
||||||
|
Name: "test",
|
||||||
|
}
|
||||||
|
versions := []map[string]map[string]recipePkg.ServiceMeta{
|
||||||
|
make(map[string]map[string]recipePkg.ServiceMeta),
|
||||||
|
make(map[string]map[string]recipePkg.ServiceMeta),
|
||||||
|
}
|
||||||
|
versions[0]["0.0.3"] = make(map[string]recipePkg.ServiceMeta)
|
||||||
|
versions[1]["0.0.2"] = make(map[string]recipePkg.ServiceMeta)
|
||||||
|
catalogue := make(recipePkg.RecipeCatalogue)
|
||||||
|
catalogue["test"] = recipePkg.RecipeMeta{
|
||||||
|
Versions: versions,
|
||||||
|
}
|
||||||
|
version, _ := getLatestVersion(recipe, catalogue)
|
||||||
|
assert.Equal(t, version, "0.0.3")
|
||||||
|
}
|
||||||
@ -57,9 +57,7 @@ is up to the end-user to decide.
|
|||||||
|
|
||||||
The command is interactive and will show a select input which allows you to
|
The command is interactive and will show a select input which allows you to
|
||||||
make a seclection. Use the "?" key to see more help on navigating this
|
make a seclection. Use the "?" key to see more help on navigating this
|
||||||
interface.
|
interface.`),
|
||||||
|
|
||||||
You may invoke this command in "wizard" mode and be prompted for input.`),
|
|
||||||
Args: cobra.RangeArgs(0, 1),
|
Args: cobra.RangeArgs(0, 1),
|
||||||
ValidArgsFunction: func(
|
ValidArgsFunction: func(
|
||||||
cmd *cobra.Command,
|
cmd *cobra.Command,
|
||||||
|
|||||||
2
go.mod
2
go.mod
@ -8,7 +8,6 @@ require (
|
|||||||
coopcloud.tech/tagcmp v0.0.0-20250818180036-0ec1b205b5ca
|
coopcloud.tech/tagcmp v0.0.0-20250818180036-0ec1b205b5ca
|
||||||
git.coopcloud.tech/toolshed/godotenv v1.5.2-0.20250103171850-4d0ca41daa5c
|
git.coopcloud.tech/toolshed/godotenv v1.5.2-0.20250103171850-4d0ca41daa5c
|
||||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||||
github.com/charmbracelet/bubbles v0.21.0
|
|
||||||
github.com/charmbracelet/bubbletea v1.3.10
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
github.com/charmbracelet/log v0.4.2
|
github.com/charmbracelet/log v0.4.2
|
||||||
@ -42,6 +41,7 @@ require (
|
|||||||
github.com/charmbracelet/colorprofile v0.3.2 // indirect
|
github.com/charmbracelet/colorprofile v0.3.2 // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.10.2 // indirect
|
github.com/charmbracelet/x/ansi v0.10.2 // indirect
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
||||||
|
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||||
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
|
||||||
github.com/cloudflare/circl v1.6.1 // indirect
|
github.com/cloudflare/circl v1.6.1 // indirect
|
||||||
|
|||||||
2
go.sum
2
go.sum
@ -133,8 +133,6 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf
|
|||||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
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 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
|
|
||||||
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
|
||||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||||
github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI=
|
github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI=
|
||||||
|
|||||||
@ -633,6 +633,11 @@ func (a App) WipeRecipeVersion() error {
|
|||||||
|
|
||||||
// WriteRecipeVersion writes the recipe version to the app .env file.
|
// WriteRecipeVersion writes the recipe version to the app .env file.
|
||||||
func (a App) WriteRecipeVersion(version string, dryRun bool) error {
|
func (a App) WriteRecipeVersion(version string, dryRun bool) error {
|
||||||
|
if version == config.UNKNOWN_DEFAULT {
|
||||||
|
log.Debug(i18n.G("version is unknown, skipping env write"))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
file, err := os.Open(a.Path)
|
file, err := os.Open(a.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@ -224,3 +224,16 @@ func TestWriteRecipeVersionOverwrite(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, "foo", app.Recipe.EnvVersion)
|
assert.Equal(t, "foo", app.Recipe.EnvVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWriteRecipeVersionUnknown(t *testing.T) {
|
||||||
|
app, err := appPkg.GetApp(testPkg.ExpectedAppFiles, testPkg.AppName)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.WriteRecipeVersion(config.UNKNOWN_DEFAULT, false); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NotEqual(t, config.UNKNOWN_DEFAULT, app.Recipe.EnvVersion)
|
||||||
|
}
|
||||||
|
|||||||
@ -37,18 +37,27 @@ func WithTimeout(timeout int) Opt {
|
|||||||
// New initiates a new Docker client. New client connections are validated so
|
// New initiates a new Docker client. New client connections are validated so
|
||||||
// that we ensure connections via SSH to the daemon can succeed. It takes into
|
// that we ensure connections via SSH to the daemon can succeed. It takes into
|
||||||
// account that you may only want the local client and not communicate via SSH.
|
// account that you may only want the local client and not communicate via SSH.
|
||||||
// For this use-case, please pass "default" as the contextName.
|
// For this use-case, please pass "default" as the serverName.
|
||||||
func New(serverName string, opts ...Opt) (*client.Client, error) {
|
func New(serverName string, opts ...Opt) (*client.Client, error) {
|
||||||
var clientOpts []client.Opt
|
var clientOpts []client.Opt
|
||||||
|
|
||||||
ctx, err := GetContext(serverName)
|
ctx, err := GetContext(serverName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
serverDir := path.Join(config.SERVERS_DIR, serverName)
|
serverDir := path.Join(config.SERVERS_DIR, serverName)
|
||||||
if _, err := os.Stat(serverDir); err == nil {
|
if _, err := os.Stat(serverDir); err != nil {
|
||||||
return nil, errors.New(i18n.G("server missing context, run \"abra server add %s\"?", serverName))
|
return nil, errors.New(i18n.G("server missing, run \"abra server add %s\"?", serverName))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, errors.New(i18n.G("unknown server, run \"abra server add %s\"?", serverName))
|
// NOTE(p4u1): when the docker context does not exist but the server folder
|
||||||
|
// is there, let's create a new docker context.
|
||||||
|
if err = CreateContext(serverName); err != nil {
|
||||||
|
return nil, errors.New(i18n.G("server missing context, context creation failed: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, err = GetContext(serverName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New(i18n.G("server missing context, run \"abra server add %s\"?", serverName))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctxEndpoint, err := contextPkg.GetContextEndpoint(ctx)
|
ctxEndpoint, err := contextPkg.GetContextEndpoint(ctx)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -403,15 +403,18 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
|
|||||||
Branch: plumbing.ReferenceName(ref.Name()),
|
Branch: plumbing.ReferenceName(ref.Name()),
|
||||||
}
|
}
|
||||||
if err := worktree.Checkout(checkOutOpts); err != nil {
|
if err := worktree.Checkout(checkOutOpts); err != nil {
|
||||||
log.Debug(i18n.G("failed to check out %s in %s", tag, r.Dir))
|
log.Debug(i18n.G("failed to check out %s in %s: %s", tag, r.Dir, err))
|
||||||
return err
|
warnMsg = append(warnMsg, i18n.G("skipping tag %s: checkout failed: %s", tag, err))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug(i18n.G("git checkout: %s in %s", ref.Name(), r.Dir))
|
log.Debug(i18n.G("git checkout: %s in %s", ref.Name(), r.Dir))
|
||||||
|
|
||||||
config, err := r.GetComposeConfig(nil)
|
config, err := r.GetComposeConfig(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
log.Debug(i18n.G("failed to get compose config for %s: %s", tag, err))
|
||||||
|
warnMsg = append(warnMsg, i18n.G("skipping tag %s: invalid compose config: %s", tag, err))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
versionMeta := make(map[string]ServiceMeta)
|
versionMeta := make(map[string]ServiceMeta)
|
||||||
@ -419,7 +422,9 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
|
|||||||
|
|
||||||
img, err := reference.ParseNormalizedNamed(service.Image)
|
img, err := reference.ParseNormalizedNamed(service.Image)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
log.Debug(i18n.G("failed to parse image for %s in %s: %s", service.Name, tag, err))
|
||||||
|
warnMsg = append(warnMsg, i18n.G("skipping tag %s: invalid image reference in service %s: %s", tag, service.Name, err))
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
path := reference.Path(img)
|
path := reference.Path(img)
|
||||||
@ -445,6 +450,7 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
|
log.Warn(i18n.G("GetRecipeVersions encountered error for %s: %s (collected %d versions)", r.Name, err, len(versions)))
|
||||||
return versions, warnMsg, nil
|
return versions, warnMsg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,6 @@ import (
|
|||||||
"coopcloud.tech/abra/pkg/formatter"
|
"coopcloud.tech/abra/pkg/formatter"
|
||||||
"coopcloud.tech/abra/pkg/i18n"
|
"coopcloud.tech/abra/pkg/i18n"
|
||||||
"coopcloud.tech/abra/pkg/logs"
|
"coopcloud.tech/abra/pkg/logs"
|
||||||
"github.com/charmbracelet/bubbles/viewport"
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/docker/cli/cli/command/service/progress"
|
"github.com/docker/cli/cli/command/service/progress"
|
||||||
containerTypes "github.com/docker/docker/api/types/container"
|
containerTypes "github.com/docker/docker/api/types/container"
|
||||||
@ -42,12 +41,6 @@ type ServiceMeta struct {
|
|||||||
ID string
|
ID string
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
statusMode = iota
|
|
||||||
logsMode = iota
|
|
||||||
errorsMode = iota
|
|
||||||
)
|
|
||||||
|
|
||||||
type Model struct {
|
type Model struct {
|
||||||
appName string
|
appName string
|
||||||
cl *dockerClient.Client
|
cl *dockerClient.Client
|
||||||
@ -56,10 +49,6 @@ type Model struct {
|
|||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
width int
|
width int
|
||||||
filters filters.Args
|
filters filters.Args
|
||||||
mode int
|
|
||||||
|
|
||||||
logsViewport viewport.Model
|
|
||||||
logsViewportReady bool
|
|
||||||
|
|
||||||
Streams *[]stream
|
Streams *[]stream
|
||||||
Logs *[]string
|
Logs *[]string
|
||||||
@ -247,10 +236,7 @@ func deployTimeout(m Model) tea.Msg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
var (
|
var cmds []tea.Cmd
|
||||||
cmd tea.Cmd
|
|
||||||
cmds []tea.Cmd
|
|
||||||
)
|
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
@ -258,25 +244,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case "ctrl+c", "q":
|
case "ctrl+c", "q":
|
||||||
m.Quit = true
|
m.Quit = true
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
case "s":
|
|
||||||
m.mode = statusMode
|
|
||||||
case "l":
|
|
||||||
m.mode = logsMode
|
|
||||||
case "e":
|
|
||||||
m.mode = errorsMode
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
m.width = msg.Width
|
m.width = msg.Width
|
||||||
|
|
||||||
if !m.logsViewportReady {
|
|
||||||
m.logsViewport = viewport.New(msg.Width, 20)
|
|
||||||
m.logsViewportReady = true
|
|
||||||
} else {
|
|
||||||
m.logsViewport.Width = msg.Width
|
|
||||||
m.logsViewport.Height = 20
|
|
||||||
}
|
|
||||||
|
|
||||||
case progressCompleteMsg:
|
case progressCompleteMsg:
|
||||||
if msg.failed {
|
if msg.failed {
|
||||||
m.Failed = true
|
m.Failed = true
|
||||||
@ -284,9 +256,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
|
|
||||||
m.count += 1
|
m.count += 1
|
||||||
|
|
||||||
// if m.complete() {
|
if m.complete() {
|
||||||
// return m, tea.Quit
|
return m, tea.Quit
|
||||||
// }
|
}
|
||||||
|
|
||||||
case timeoutMsg:
|
case timeoutMsg:
|
||||||
m.TimedOut = true
|
m.TimedOut = true
|
||||||
@ -346,46 +318,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.logsViewport, cmd = m.logsViewport.Update(msg)
|
|
||||||
cmds = append(cmds, cmd)
|
|
||||||
|
|
||||||
return m, tea.Batch(cmds...)
|
return m, tea.Batch(cmds...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) View() string {
|
func (m Model) View() string {
|
||||||
body := strings.Builder{}
|
body := strings.Builder{}
|
||||||
|
|
||||||
body.WriteString("menu: [s]tatus [l]ogs [e]rrors\n")
|
|
||||||
|
|
||||||
var res string
|
|
||||||
switch {
|
|
||||||
case m.mode == statusMode:
|
|
||||||
res = statusView(m)
|
|
||||||
case m.mode == logsMode:
|
|
||||||
res = logsView(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
return body.String() + res
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func logsView(m Model) string {
|
|
||||||
body := strings.Builder{}
|
|
||||||
m.logsViewport.SetContent(strings.Join(*m.Logs, "\n"))
|
|
||||||
m.logsViewport.GotoBottom()
|
|
||||||
body.WriteString(m.logsViewport.View())
|
|
||||||
return body.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func errorsView(m Model) string {
|
|
||||||
body := strings.Builder{}
|
|
||||||
body.WriteString("ERRORS COMING SOON")
|
|
||||||
return body.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func statusView(m Model) string {
|
|
||||||
body := strings.Builder{}
|
|
||||||
|
|
||||||
for _, stream := range *m.Streams {
|
for _, stream := range *m.Streams {
|
||||||
split := strings.Split(stream.Name, "_")
|
split := strings.Split(stream.Name, "_")
|
||||||
short := split[len(split)-1]
|
short := split[len(split)-1]
|
||||||
|
|||||||
@ -201,6 +201,7 @@ func RunDeploy(
|
|||||||
appName string,
|
appName string,
|
||||||
serverName string,
|
serverName string,
|
||||||
dontWait bool,
|
dontWait bool,
|
||||||
|
noInput bool,
|
||||||
filters filters.Args,
|
filters filters.Args,
|
||||||
) error {
|
) error {
|
||||||
log.Info(i18n.G("initialising deployment"))
|
log.Info(i18n.G("initialising deployment"))
|
||||||
@ -226,6 +227,7 @@ func RunDeploy(
|
|||||||
appName,
|
appName,
|
||||||
serverName,
|
serverName,
|
||||||
dontWait,
|
dontWait,
|
||||||
|
noInput,
|
||||||
filters,
|
filters,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -248,6 +250,7 @@ func deployCompose(
|
|||||||
appName string,
|
appName string,
|
||||||
serverName string,
|
serverName string,
|
||||||
dontWait bool,
|
dontWait bool,
|
||||||
|
noInput bool,
|
||||||
filters filters.Args,
|
filters filters.Args,
|
||||||
) error {
|
) error {
|
||||||
namespace := convert.NewNamespace(opts.Namespace)
|
namespace := convert.NewNamespace(opts.Namespace)
|
||||||
@ -311,6 +314,7 @@ func deployCompose(
|
|||||||
Services: serviceIDs,
|
Services: serviceIDs,
|
||||||
AppName: appName,
|
AppName: appName,
|
||||||
ServerName: serverName,
|
ServerName: serverName,
|
||||||
|
NoInput: noInput,
|
||||||
Filters: filters,
|
Filters: filters,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -561,6 +565,7 @@ func timestamp() string {
|
|||||||
type WaitOpts struct {
|
type WaitOpts struct {
|
||||||
AppName string
|
AppName string
|
||||||
Filters filters.Args
|
Filters filters.Args
|
||||||
|
NoInput bool
|
||||||
NoLog bool
|
NoLog bool
|
||||||
Quiet bool
|
Quiet bool
|
||||||
ServerName string
|
ServerName string
|
||||||
@ -570,7 +575,13 @@ type WaitOpts struct {
|
|||||||
func WaitOnServices(ctx context.Context, cl *dockerClient.Client, opts WaitOpts) error {
|
func WaitOnServices(ctx context.Context, cl *dockerClient.Client, opts WaitOpts) error {
|
||||||
timeout := time.Duration(WaitTimeout) * time.Second
|
timeout := time.Duration(WaitTimeout) * time.Second
|
||||||
model := ui.DeployInitialModel(ctx, cl, opts.Services, opts.AppName, timeout, opts.Filters)
|
model := ui.DeployInitialModel(ctx, cl, opts.Services, opts.AppName, timeout, opts.Filters)
|
||||||
tui := tea.NewProgram(model)
|
|
||||||
|
var tui *tea.Program
|
||||||
|
if opts.NoInput {
|
||||||
|
tui = tea.NewProgram(model, tea.WithoutRenderer(), tea.WithInput(nil))
|
||||||
|
} else {
|
||||||
|
tui = tea.NewProgram(model)
|
||||||
|
}
|
||||||
|
|
||||||
if !opts.Quiet {
|
if !opts.Quiet {
|
||||||
log.Info(i18n.G("polling deployment status"))
|
log.Info(i18n.G("polling deployment status"))
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
ABRA_VERSION="0.11.0-beta"
|
ABRA_VERSION="0.12.0-beta"
|
||||||
ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/toolshed/abra/releases/tags/$ABRA_VERSION"
|
ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/toolshed/abra/releases/tags/$ABRA_VERSION"
|
||||||
RC_VERSION="0.11.0-beta"
|
RC_VERSION="0.12.0-beta"
|
||||||
RC_VERSION_URL="https://git.coopcloud.tech/api/v1/repos/toolshed/abra/releases/tags/$RC_VERSION"
|
RC_VERSION_URL="https://git.coopcloud.tech/api/v1/repos/toolshed/abra/releases/tags/$RC_VERSION"
|
||||||
|
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
@ -14,15 +14,15 @@ done
|
|||||||
|
|
||||||
function show_banner {
|
function show_banner {
|
||||||
echo ""
|
echo ""
|
||||||
echo " ____ ____ _ _ "
|
echo " ____ ____ _ _ "
|
||||||
echo " / ___|___ ___ _ __ / ___| | ___ _ _ __| |"
|
echo " / ___|___ ___ _ __ / ___| | ___ _ _ __| |"
|
||||||
echo " | | / _ \ _____ / _ \| '_ \ | | | |/ _ \| | | |/ _' |"
|
echo " | | / _ \ ___ / _ \| '_ \ | | | |/ _ \| | | |/ _' |"
|
||||||
echo " | |__| (_) |_____| (_) | |_) | | |___| | (_) | |_| | (_| |"
|
echo " | |__| (_) |___| (_) | |_) | | |___| | (_) | |_| | (_| |"
|
||||||
echo " \____\___/ \___/| .__/ \____|_|\___/ \__,_|\__,_|"
|
echo " \____\___/ \___/| .__/ \____|_|\___/ \__,_|\__,_|"
|
||||||
echo " |_|"
|
echo " |_|"
|
||||||
echo ""
|
echo ""
|
||||||
echo ""
|
echo ""
|
||||||
echo " === Public interest infrastructure === "
|
echo " === Public interest infrastructure === "
|
||||||
echo ""
|
echo ""
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
@ -89,7 +89,7 @@ function install_abra_release {
|
|||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo "$(tput setaf 3)WARNING: $HOME/.local/bin/ is not in \$PATH! If you want to run abra by just typing "abra" you should add it to your \$PATH! To do that run this once and restart your terminal:$(tput sgr0)"
|
echo "$(tput setaf 3)WARNING: $HOME/.local/bin/ is not in \$PATH! If you want to run abra by just typing "abra" you should add it to your \$PATH! To do that run this once and restart your terminal:$(tput sgr0)"
|
||||||
p=$HOME/.local/bin
|
p=$HOME/.local/bin
|
||||||
com="echo PATH=\$PATH:$p"
|
com='echo PATH="$PATH:'"$p"'"'
|
||||||
if [[ $SHELL =~ "bash" ]]; then
|
if [[ $SHELL =~ "bash" ]]; then
|
||||||
echo "$com >> $HOME/.bashrc"
|
echo "$com >> $HOME/.bashrc"
|
||||||
elif [[ $SHELL =~ "fizsh" ]]; then
|
elif [[ $SHELL =~ "fizsh" ]]; then
|
||||||
|
|||||||
@ -543,7 +543,7 @@ teardown(){
|
|||||||
|
|
||||||
# bats test_tags=slow
|
# bats test_tags=slow
|
||||||
@test "ignore timeout when not present in env" {
|
@test "ignore timeout when not present in env" {
|
||||||
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks --debug
|
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
|
||||||
assert_success
|
assert_success
|
||||||
refute_output --partial "timeout: set to"
|
refute_output --partial "timeout: set to"
|
||||||
}
|
}
|
||||||
@ -554,6 +554,7 @@ teardown(){
|
|||||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||||
assert_success
|
assert_success
|
||||||
|
|
||||||
|
# NOTE(d1}: --debug required
|
||||||
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks --debug
|
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks --debug
|
||||||
assert_success
|
assert_success
|
||||||
assert_output --partial "timeout: set to 120"
|
assert_output --partial "timeout: set to 120"
|
||||||
@ -579,16 +580,25 @@ teardown(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
# bats test_tags=slow
|
# bats test_tags=slow
|
||||||
@test "manually created server without context bails gracefully" {
|
@test "re-deploy updates existing env vars" {
|
||||||
run mkdir -p "$ABRA_DIR/servers/default2"
|
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input
|
||||||
assert_success
|
assert_success
|
||||||
assert_exists "$ABRA_DIR/servers/default2"
|
|
||||||
|
|
||||||
run cp "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" "$ABRA_DIR/servers/default2/$TEST_APP_DOMAIN_2.env"
|
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
|
||||||
|
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
|
||||||
assert_success
|
assert_success
|
||||||
assert_exists "$ABRA_DIR/servers/default2/$TEST_APP_DOMAIN_2.env"
|
assert_output --partial "WITH_COMMENT=foo"
|
||||||
|
|
||||||
run $ABRA app deploy "$TEST_APP_DOMAIN_2" --no-input --no-converge-checks
|
run sed -i 's/WITH_COMMENT=foo/WITH_COMMENT=bar/g' \
|
||||||
assert_failure
|
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||||
assert_output --partial "server missing context"
|
assert_success
|
||||||
|
|
||||||
|
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --force
|
||||||
|
assert_success
|
||||||
|
|
||||||
|
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
|
||||||
|
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
|
||||||
|
assert_success
|
||||||
|
refute_output --partial "WITH_COMMENT=foo"
|
||||||
|
assert_output --partial "WITH_COMMENT=bar"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -68,6 +68,13 @@ teardown(){
|
|||||||
assert_success
|
assert_success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@test "domain shown with https" {
|
||||||
|
run $ABRA app deploy "$TEST_APP_DOMAIN" \
|
||||||
|
--no-input --no-converge-checks
|
||||||
|
assert_success
|
||||||
|
assert_output --partial "https://$TEST_DOMAIN"
|
||||||
|
}
|
||||||
|
|
||||||
# bats test_tags=slow
|
# bats test_tags=slow
|
||||||
@test "show changed config version on re-deploy" {
|
@test "show changed config version on re-deploy" {
|
||||||
run $ABRA app deploy "$TEST_APP_DOMAIN" \
|
run $ABRA app deploy "$TEST_APP_DOMAIN" \
|
||||||
|
|||||||
@ -160,23 +160,6 @@ teardown(){
|
|||||||
assert_not_exists "$ABRA_DIR/servers/foo.com"
|
assert_not_exists "$ABRA_DIR/servers/foo.com"
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "list with status skips unknown servers" {
|
|
||||||
if [[ ! -d "$ABRA_DIR/servers/foo" ]]; then
|
|
||||||
run mkdir -p "$ABRA_DIR/servers/foo"
|
|
||||||
assert_success
|
|
||||||
assert_exists "$ABRA_DIR/servers/foo"
|
|
||||||
|
|
||||||
run cp "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" \
|
|
||||||
"$ABRA_DIR/servers/foo/$TEST_APP_DOMAIN.env"
|
|
||||||
assert_success
|
|
||||||
assert_exists "$ABRA_DIR/servers/foo/$TEST_APP_DOMAIN.env"
|
|
||||||
fi
|
|
||||||
|
|
||||||
run $ABRA app ls --status
|
|
||||||
assert_success
|
|
||||||
assert_output --partial "server missing context"
|
|
||||||
}
|
|
||||||
|
|
||||||
# bats test_tags=slow
|
# bats test_tags=slow
|
||||||
@test "list does not fail if missing .env" {
|
@test "list does not fail if missing .env" {
|
||||||
_deploy_app
|
_deploy_app
|
||||||
|
|||||||
@ -19,6 +19,10 @@ setup(){
|
|||||||
teardown(){
|
teardown(){
|
||||||
_reset_recipe
|
_reset_recipe
|
||||||
_reset_tags
|
_reset_tags
|
||||||
|
if [[ -d "$ABRA_DIR/recipes/foobar" ]]; then
|
||||||
|
run rm -rf "$ABRA_DIR/recipes/foobar"
|
||||||
|
assert_success
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "validate recipe argument" {
|
@test "validate recipe argument" {
|
||||||
@ -126,3 +130,71 @@ teardown(){
|
|||||||
assert_line --index 0 --partial 'synced label'
|
assert_line --index 0 --partial 'synced label'
|
||||||
refute_line --index 1 --partial 'synced label'
|
refute_line --index 1 --partial 'synced label'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@test "sync with no tags or previous release" {
|
||||||
|
_remove_tags
|
||||||
|
|
||||||
|
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --patch
|
||||||
|
assert_success
|
||||||
|
|
||||||
|
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
|
||||||
|
assert_success
|
||||||
|
assert_output --partial 'image: nginx:1.21.6'
|
||||||
|
|
||||||
|
# NOTE(d1): ensure the latest tag is the one we expect
|
||||||
|
_remove_tags
|
||||||
|
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag \
|
||||||
|
-a "0.3.0+1.21.0" -m "fake: 0.3.0+1.21.0"
|
||||||
|
assert_success
|
||||||
|
|
||||||
|
run $ABRA recipe sync "$TEST_RECIPE" --no-input --patch
|
||||||
|
assert_success
|
||||||
|
|
||||||
|
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
|
||||||
|
assert_success
|
||||||
|
assert_output --regexp 'coop-cloud\.\$\{STACK_NAME\}\.version=0\.3\.1\+1\.2.*'
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "sync recipe without input fails with prompt" {
|
||||||
|
run $ABRA recipe new foobar
|
||||||
|
assert_success
|
||||||
|
assert_exists "$ABRA_DIR/recipes/foobar"
|
||||||
|
|
||||||
|
run $ABRA recipe sync foobar --no-input --patch
|
||||||
|
assert_failure
|
||||||
|
assert_output --partial "input required for initial version"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "sync new recipe: development release" {
|
||||||
|
run $ABRA recipe new foobar
|
||||||
|
assert_success
|
||||||
|
assert_exists "$ABRA_DIR/recipes/foobar"
|
||||||
|
|
||||||
|
run bash -c "echo 0.1.0 | $ABRA recipe sync foobar --patch"
|
||||||
|
assert_success
|
||||||
|
assert_output --regexp 'coop-cloud\.\$\{STACK_NAME\}\.version=0\.1\.0\+1\.2.*'
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "sync new recipe: public release" {
|
||||||
|
run $ABRA recipe new foobar
|
||||||
|
assert_success
|
||||||
|
assert_exists "$ABRA_DIR/recipes/foobar"
|
||||||
|
|
||||||
|
run bash -c "echo 1.0.0 | $ABRA recipe sync foobar --patch"
|
||||||
|
assert_success
|
||||||
|
assert_output --regexp 'coop-cloud\.\$\{STACK_NAME\}\.version=1\.0\.0\+1\.2.*'
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "sync newly created recipe with no version label" {
|
||||||
|
run $ABRA recipe new foobar
|
||||||
|
assert_success
|
||||||
|
assert_exists "$ABRA_DIR/recipes/foobar"
|
||||||
|
|
||||||
|
run sed -i 's/- "coop-cloud.${STACK_NAME}.version="/#- "coop-cloud.${STACK_NAME}.version="/g' \
|
||||||
|
"$ABRA_DIR/recipes/foobar/compose.yml"
|
||||||
|
assert_success
|
||||||
|
|
||||||
|
run bash -c "echo 0.1.0 | $ABRA recipe sync foobar --patch"
|
||||||
|
assert_failure
|
||||||
|
assert_output --partial "automagic insertion not supported yet"
|
||||||
|
}
|
||||||
|
|||||||
21
vendor/github.com/charmbracelet/bubbles/LICENSE
generated
vendored
21
vendor/github.com/charmbracelet/bubbles/LICENSE
generated
vendored
@ -1,21 +0,0 @@
|
|||||||
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.
|
|
||||||
140
vendor/github.com/charmbracelet/bubbles/key/key.go
generated
vendored
140
vendor/github.com/charmbracelet/bubbles/key/key.go
generated
vendored
@ -1,140 +0,0 @@
|
|||||||
// Package key provides some types and functions for generating user-definable
|
|
||||||
// keymappings useful in Bubble Tea components. There are a few different ways
|
|
||||||
// you can define a keymapping with this package. Here's one example:
|
|
||||||
//
|
|
||||||
// type KeyMap struct {
|
|
||||||
// Up key.Binding
|
|
||||||
// Down key.Binding
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// var DefaultKeyMap = KeyMap{
|
|
||||||
// Up: key.NewBinding(
|
|
||||||
// key.WithKeys("k", "up"), // actual keybindings
|
|
||||||
// key.WithHelp("↑/k", "move up"), // corresponding help text
|
|
||||||
// ),
|
|
||||||
// Down: key.NewBinding(
|
|
||||||
// key.WithKeys("j", "down"),
|
|
||||||
// key.WithHelp("↓/j", "move down"),
|
|
||||||
// ),
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
||||||
// switch msg := msg.(type) {
|
|
||||||
// case tea.KeyMsg:
|
|
||||||
// switch {
|
|
||||||
// case key.Matches(msg, DefaultKeyMap.Up):
|
|
||||||
// // The user pressed up
|
|
||||||
// case key.Matches(msg, DefaultKeyMap.Down):
|
|
||||||
// // The user pressed down
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // ...
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// The help information, which is not used in the example above, can be used
|
|
||||||
// to render help text for keystrokes in your views.
|
|
||||||
package key
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
// Binding describes a set of keybindings and, optionally, their associated
|
|
||||||
// help text.
|
|
||||||
type Binding struct {
|
|
||||||
keys []string
|
|
||||||
help Help
|
|
||||||
disabled bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// BindingOpt is an initialization option for a keybinding. It's used as an
|
|
||||||
// argument to NewBinding.
|
|
||||||
type BindingOpt func(*Binding)
|
|
||||||
|
|
||||||
// NewBinding returns a new keybinding from a set of BindingOpt options.
|
|
||||||
func NewBinding(opts ...BindingOpt) Binding {
|
|
||||||
b := &Binding{}
|
|
||||||
for _, opt := range opts {
|
|
||||||
opt(b)
|
|
||||||
}
|
|
||||||
return *b
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithKeys initializes a keybinding with the given keystrokes.
|
|
||||||
func WithKeys(keys ...string) BindingOpt {
|
|
||||||
return func(b *Binding) {
|
|
||||||
b.keys = keys
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithHelp initializes a keybinding with the given help text.
|
|
||||||
func WithHelp(key, desc string) BindingOpt {
|
|
||||||
return func(b *Binding) {
|
|
||||||
b.help = Help{Key: key, Desc: desc}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithDisabled initializes a disabled keybinding.
|
|
||||||
func WithDisabled() BindingOpt {
|
|
||||||
return func(b *Binding) {
|
|
||||||
b.disabled = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetKeys sets the keys for the keybinding.
|
|
||||||
func (b *Binding) SetKeys(keys ...string) {
|
|
||||||
b.keys = keys
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keys returns the keys for the keybinding.
|
|
||||||
func (b Binding) Keys() []string {
|
|
||||||
return b.keys
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetHelp sets the help text for the keybinding.
|
|
||||||
func (b *Binding) SetHelp(key, desc string) {
|
|
||||||
b.help = Help{Key: key, Desc: desc}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Help returns the Help information for the keybinding.
|
|
||||||
func (b Binding) Help() Help {
|
|
||||||
return b.help
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enabled returns whether or not the keybinding is enabled. Disabled
|
|
||||||
// keybindings won't be activated and won't show up in help. Keybindings are
|
|
||||||
// enabled by default.
|
|
||||||
func (b Binding) Enabled() bool {
|
|
||||||
return !b.disabled && b.keys != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetEnabled enables or disables the keybinding.
|
|
||||||
func (b *Binding) SetEnabled(v bool) {
|
|
||||||
b.disabled = !v
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unbind removes the keys and help from this binding, effectively nullifying
|
|
||||||
// it. This is a step beyond disabling it, since applications can enable
|
|
||||||
// or disable key bindings based on application state.
|
|
||||||
func (b *Binding) Unbind() {
|
|
||||||
b.keys = nil
|
|
||||||
b.help = Help{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Help is help information for a given keybinding.
|
|
||||||
type Help struct {
|
|
||||||
Key string
|
|
||||||
Desc string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Matches checks if the given key matches the given bindings.
|
|
||||||
func Matches[Key fmt.Stringer](k Key, b ...Binding) bool {
|
|
||||||
keys := k.String()
|
|
||||||
for _, binding := range b {
|
|
||||||
for _, v := range binding.keys {
|
|
||||||
if keys == v && binding.Enabled() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
60
vendor/github.com/charmbracelet/bubbles/viewport/keymap.go
generated
vendored
60
vendor/github.com/charmbracelet/bubbles/viewport/keymap.go
generated
vendored
@ -1,60 +0,0 @@
|
|||||||
// Package viewport provides a component for rendering a viewport in a Bubble
|
|
||||||
// Tea.
|
|
||||||
package viewport
|
|
||||||
|
|
||||||
import "github.com/charmbracelet/bubbles/key"
|
|
||||||
|
|
||||||
const spacebar = " "
|
|
||||||
|
|
||||||
// KeyMap defines the keybindings for the viewport. Note that you don't
|
|
||||||
// necessary need to use keybindings at all; the viewport can be controlled
|
|
||||||
// programmatically with methods like Model.LineDown(1). See the GoDocs for
|
|
||||||
// details.
|
|
||||||
type KeyMap struct {
|
|
||||||
PageDown key.Binding
|
|
||||||
PageUp key.Binding
|
|
||||||
HalfPageUp key.Binding
|
|
||||||
HalfPageDown key.Binding
|
|
||||||
Down key.Binding
|
|
||||||
Up key.Binding
|
|
||||||
Left key.Binding
|
|
||||||
Right key.Binding
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultKeyMap returns a set of pager-like default keybindings.
|
|
||||||
func DefaultKeyMap() KeyMap {
|
|
||||||
return KeyMap{
|
|
||||||
PageDown: key.NewBinding(
|
|
||||||
key.WithKeys("pgdown", spacebar, "f"),
|
|
||||||
key.WithHelp("f/pgdn", "page down"),
|
|
||||||
),
|
|
||||||
PageUp: key.NewBinding(
|
|
||||||
key.WithKeys("pgup", "b"),
|
|
||||||
key.WithHelp("b/pgup", "page up"),
|
|
||||||
),
|
|
||||||
HalfPageUp: key.NewBinding(
|
|
||||||
key.WithKeys("u", "ctrl+u"),
|
|
||||||
key.WithHelp("u", "½ page up"),
|
|
||||||
),
|
|
||||||
HalfPageDown: key.NewBinding(
|
|
||||||
key.WithKeys("d", "ctrl+d"),
|
|
||||||
key.WithHelp("d", "½ page down"),
|
|
||||||
),
|
|
||||||
Up: key.NewBinding(
|
|
||||||
key.WithKeys("up", "k"),
|
|
||||||
key.WithHelp("↑/k", "up"),
|
|
||||||
),
|
|
||||||
Down: key.NewBinding(
|
|
||||||
key.WithKeys("down", "j"),
|
|
||||||
key.WithHelp("↓/j", "down"),
|
|
||||||
),
|
|
||||||
Left: key.NewBinding(
|
|
||||||
key.WithKeys("left", "h"),
|
|
||||||
key.WithHelp("←/h", "move left"),
|
|
||||||
),
|
|
||||||
Right: key.NewBinding(
|
|
||||||
key.WithKeys("right", "l"),
|
|
||||||
key.WithHelp("→/l", "move right"),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
544
vendor/github.com/charmbracelet/bubbles/viewport/viewport.go
generated
vendored
544
vendor/github.com/charmbracelet/bubbles/viewport/viewport.go
generated
vendored
@ -1,544 +0,0 @@
|
|||||||
package viewport
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/key"
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
|
||||||
"github.com/charmbracelet/x/ansi"
|
|
||||||
)
|
|
||||||
|
|
||||||
// New returns a new model with the given width and height as well as default
|
|
||||||
// key mappings.
|
|
||||||
func New(width, height int) (m Model) {
|
|
||||||
m.Width = width
|
|
||||||
m.Height = height
|
|
||||||
m.setInitialValues()
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
// Model is the Bubble Tea model for this viewport element.
|
|
||||||
type Model struct {
|
|
||||||
Width int
|
|
||||||
Height int
|
|
||||||
KeyMap KeyMap
|
|
||||||
|
|
||||||
// Whether or not to respond to the mouse. The mouse must be enabled in
|
|
||||||
// Bubble Tea for this to work. For details, see the Bubble Tea docs.
|
|
||||||
MouseWheelEnabled bool
|
|
||||||
|
|
||||||
// The number of lines the mouse wheel will scroll. By default, this is 3.
|
|
||||||
MouseWheelDelta int
|
|
||||||
|
|
||||||
// YOffset is the vertical scroll position.
|
|
||||||
YOffset int
|
|
||||||
|
|
||||||
// xOffset is the horizontal scroll position.
|
|
||||||
xOffset int
|
|
||||||
|
|
||||||
// horizontalStep is the number of columns we move left or right during a
|
|
||||||
// default horizontal scroll.
|
|
||||||
horizontalStep int
|
|
||||||
|
|
||||||
// YPosition is the position of the viewport in relation to the terminal
|
|
||||||
// window. It's used in high performance rendering only.
|
|
||||||
YPosition int
|
|
||||||
|
|
||||||
// Style applies a lipgloss style to the viewport. Realistically, it's most
|
|
||||||
// useful for setting borders, margins and padding.
|
|
||||||
Style lipgloss.Style
|
|
||||||
|
|
||||||
// HighPerformanceRendering bypasses the normal Bubble Tea renderer to
|
|
||||||
// provide higher performance rendering. Most of the time the normal Bubble
|
|
||||||
// Tea rendering methods will suffice, but if you're passing content with
|
|
||||||
// a lot of ANSI escape codes you may see improved rendering in certain
|
|
||||||
// terminals with this enabled.
|
|
||||||
//
|
|
||||||
// This should only be used in program occupying the entire terminal,
|
|
||||||
// which is usually via the alternate screen buffer.
|
|
||||||
//
|
|
||||||
// Deprecated: high performance rendering is now deprecated in Bubble Tea.
|
|
||||||
HighPerformanceRendering bool
|
|
||||||
|
|
||||||
initialized bool
|
|
||||||
lines []string
|
|
||||||
longestLineWidth int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) setInitialValues() {
|
|
||||||
m.KeyMap = DefaultKeyMap()
|
|
||||||
m.MouseWheelEnabled = true
|
|
||||||
m.MouseWheelDelta = 3
|
|
||||||
m.initialized = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init exists to satisfy the tea.Model interface for composability purposes.
|
|
||||||
func (m Model) Init() tea.Cmd {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AtTop returns whether or not the viewport is at the very top position.
|
|
||||||
func (m Model) AtTop() bool {
|
|
||||||
return m.YOffset <= 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// AtBottom returns whether or not the viewport is at or past the very bottom
|
|
||||||
// position.
|
|
||||||
func (m Model) AtBottom() bool {
|
|
||||||
return m.YOffset >= m.maxYOffset()
|
|
||||||
}
|
|
||||||
|
|
||||||
// PastBottom returns whether or not the viewport is scrolled beyond the last
|
|
||||||
// line. This can happen when adjusting the viewport height.
|
|
||||||
func (m Model) PastBottom() bool {
|
|
||||||
return m.YOffset > m.maxYOffset()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ScrollPercent returns the amount scrolled as a float between 0 and 1.
|
|
||||||
func (m Model) ScrollPercent() float64 {
|
|
||||||
if m.Height >= len(m.lines) {
|
|
||||||
return 1.0
|
|
||||||
}
|
|
||||||
y := float64(m.YOffset)
|
|
||||||
h := float64(m.Height)
|
|
||||||
t := float64(len(m.lines))
|
|
||||||
v := y / (t - h)
|
|
||||||
return math.Max(0.0, math.Min(1.0, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// HorizontalScrollPercent returns the amount horizontally scrolled as a float
|
|
||||||
// between 0 and 1.
|
|
||||||
func (m Model) HorizontalScrollPercent() float64 {
|
|
||||||
if m.xOffset >= m.longestLineWidth-m.Width {
|
|
||||||
return 1.0
|
|
||||||
}
|
|
||||||
y := float64(m.xOffset)
|
|
||||||
h := float64(m.Width)
|
|
||||||
t := float64(m.longestLineWidth)
|
|
||||||
v := y / (t - h)
|
|
||||||
return math.Max(0.0, math.Min(1.0, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetContent set the pager's text content.
|
|
||||||
func (m *Model) SetContent(s string) {
|
|
||||||
s = strings.ReplaceAll(s, "\r\n", "\n") // normalize line endings
|
|
||||||
m.lines = strings.Split(s, "\n")
|
|
||||||
m.longestLineWidth = findLongestLineWidth(m.lines)
|
|
||||||
|
|
||||||
if m.YOffset > len(m.lines)-1 {
|
|
||||||
m.GotoBottom()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// maxYOffset returns the maximum possible value of the y-offset based on the
|
|
||||||
// viewport's content and set height.
|
|
||||||
func (m Model) maxYOffset() int {
|
|
||||||
return max(0, len(m.lines)-m.Height+m.Style.GetVerticalFrameSize())
|
|
||||||
}
|
|
||||||
|
|
||||||
// visibleLines returns the lines that should currently be visible in the
|
|
||||||
// viewport.
|
|
||||||
func (m Model) visibleLines() (lines []string) {
|
|
||||||
h := m.Height - m.Style.GetVerticalFrameSize()
|
|
||||||
w := m.Width - m.Style.GetHorizontalFrameSize()
|
|
||||||
|
|
||||||
if len(m.lines) > 0 {
|
|
||||||
top := max(0, m.YOffset)
|
|
||||||
bottom := clamp(m.YOffset+h, top, len(m.lines))
|
|
||||||
lines = m.lines[top:bottom]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (m.xOffset == 0 && m.longestLineWidth <= w) || w == 0 {
|
|
||||||
return lines
|
|
||||||
}
|
|
||||||
|
|
||||||
cutLines := make([]string, len(lines))
|
|
||||||
for i := range lines {
|
|
||||||
cutLines[i] = ansi.Cut(lines[i], m.xOffset, m.xOffset+w)
|
|
||||||
}
|
|
||||||
return cutLines
|
|
||||||
}
|
|
||||||
|
|
||||||
// scrollArea returns the scrollable boundaries for high performance rendering.
|
|
||||||
//
|
|
||||||
// Deprecated: high performance rendering is deprecated in Bubble Tea.
|
|
||||||
func (m Model) scrollArea() (top, bottom int) {
|
|
||||||
top = max(0, m.YPosition)
|
|
||||||
bottom = max(top, top+m.Height)
|
|
||||||
if top > 0 && bottom > top {
|
|
||||||
bottom--
|
|
||||||
}
|
|
||||||
return top, bottom
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetYOffset sets the Y offset.
|
|
||||||
func (m *Model) SetYOffset(n int) {
|
|
||||||
m.YOffset = clamp(n, 0, m.maxYOffset())
|
|
||||||
}
|
|
||||||
|
|
||||||
// ViewDown moves the view down by the number of lines in the viewport.
|
|
||||||
// Basically, "page down".
|
|
||||||
//
|
|
||||||
// Deprecated: use [Model.PageDown] instead.
|
|
||||||
func (m *Model) ViewDown() []string {
|
|
||||||
return m.PageDown()
|
|
||||||
}
|
|
||||||
|
|
||||||
// PageDown moves the view down by the number of lines in the viewport.
|
|
||||||
func (m *Model) PageDown() []string {
|
|
||||||
if m.AtBottom() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return m.ScrollDown(m.Height)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ViewUp moves the view up by one height of the viewport.
|
|
||||||
// Basically, "page up".
|
|
||||||
//
|
|
||||||
// Deprecated: use [Model.PageUp] instead.
|
|
||||||
func (m *Model) ViewUp() []string {
|
|
||||||
return m.PageUp()
|
|
||||||
}
|
|
||||||
|
|
||||||
// PageUp moves the view up by one height of the viewport.
|
|
||||||
func (m *Model) PageUp() []string {
|
|
||||||
if m.AtTop() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return m.ScrollUp(m.Height)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HalfViewDown moves the view down by half the height of the viewport.
|
|
||||||
//
|
|
||||||
// Deprecated: use [Model.HalfPageDown] instead.
|
|
||||||
func (m *Model) HalfViewDown() (lines []string) {
|
|
||||||
return m.HalfPageDown()
|
|
||||||
}
|
|
||||||
|
|
||||||
// HalfPageDown moves the view down by half the height of the viewport.
|
|
||||||
func (m *Model) HalfPageDown() (lines []string) {
|
|
||||||
if m.AtBottom() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return m.ScrollDown(m.Height / 2) //nolint:mnd
|
|
||||||
}
|
|
||||||
|
|
||||||
// HalfViewUp moves the view up by half the height of the viewport.
|
|
||||||
//
|
|
||||||
// Deprecated: use [Model.HalfPageUp] instead.
|
|
||||||
func (m *Model) HalfViewUp() (lines []string) {
|
|
||||||
return m.HalfPageUp()
|
|
||||||
}
|
|
||||||
|
|
||||||
// HalfPageUp moves the view up by half the height of the viewport.
|
|
||||||
func (m *Model) HalfPageUp() (lines []string) {
|
|
||||||
if m.AtTop() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return m.ScrollUp(m.Height / 2) //nolint:mnd
|
|
||||||
}
|
|
||||||
|
|
||||||
// LineDown moves the view down by the given number of lines.
|
|
||||||
//
|
|
||||||
// Deprecated: use [Model.ScrollDown] instead.
|
|
||||||
func (m *Model) LineDown(n int) (lines []string) {
|
|
||||||
return m.ScrollDown(n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ScrollDown moves the view down by the given number of lines.
|
|
||||||
func (m *Model) ScrollDown(n int) (lines []string) {
|
|
||||||
if m.AtBottom() || n == 0 || len(m.lines) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the number of lines by which we're going to scroll isn't
|
|
||||||
// greater than the number of lines we actually have left before we reach
|
|
||||||
// the bottom.
|
|
||||||
m.SetYOffset(m.YOffset + n)
|
|
||||||
|
|
||||||
// Gather lines to send off for performance scrolling.
|
|
||||||
//
|
|
||||||
// XXX: high performance rendering is deprecated in Bubble Tea.
|
|
||||||
bottom := clamp(m.YOffset+m.Height, 0, len(m.lines))
|
|
||||||
top := clamp(m.YOffset+m.Height-n, 0, bottom)
|
|
||||||
return m.lines[top:bottom]
|
|
||||||
}
|
|
||||||
|
|
||||||
// LineUp moves the view down by the given number of lines. Returns the new
|
|
||||||
// lines to show.
|
|
||||||
//
|
|
||||||
// Deprecated: use [Model.ScrollUp] instead.
|
|
||||||
func (m *Model) LineUp(n int) (lines []string) {
|
|
||||||
return m.ScrollUp(n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ScrollUp moves the view down by the given number of lines. Returns the new
|
|
||||||
// lines to show.
|
|
||||||
func (m *Model) ScrollUp(n int) (lines []string) {
|
|
||||||
if m.AtTop() || n == 0 || len(m.lines) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the number of lines by which we're going to scroll isn't
|
|
||||||
// greater than the number of lines we are from the top.
|
|
||||||
m.SetYOffset(m.YOffset - n)
|
|
||||||
|
|
||||||
// Gather lines to send off for performance scrolling.
|
|
||||||
//
|
|
||||||
// XXX: high performance rendering is deprecated in Bubble Tea.
|
|
||||||
top := max(0, m.YOffset)
|
|
||||||
bottom := clamp(m.YOffset+n, 0, m.maxYOffset())
|
|
||||||
return m.lines[top:bottom]
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetHorizontalStep sets the default amount of columns to scroll left or right
|
|
||||||
// with the default viewport key map.
|
|
||||||
//
|
|
||||||
// If set to 0 or less, horizontal scrolling is disabled.
|
|
||||||
//
|
|
||||||
// On v1, horizontal scrolling is disabled by default.
|
|
||||||
func (m *Model) SetHorizontalStep(n int) {
|
|
||||||
m.horizontalStep = max(n, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetXOffset sets the X offset.
|
|
||||||
func (m *Model) SetXOffset(n int) {
|
|
||||||
m.xOffset = clamp(n, 0, m.longestLineWidth-m.Width)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ScrollLeft moves the viewport to the left by the given number of columns.
|
|
||||||
func (m *Model) ScrollLeft(n int) {
|
|
||||||
m.SetXOffset(m.xOffset - n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ScrollRight moves viewport to the right by the given number of columns.
|
|
||||||
func (m *Model) ScrollRight(n int) {
|
|
||||||
m.SetXOffset(m.xOffset + n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TotalLineCount returns the total number of lines (both hidden and visible) within the viewport.
|
|
||||||
func (m Model) TotalLineCount() int {
|
|
||||||
return len(m.lines)
|
|
||||||
}
|
|
||||||
|
|
||||||
// VisibleLineCount returns the number of the visible lines within the viewport.
|
|
||||||
func (m Model) VisibleLineCount() int {
|
|
||||||
return len(m.visibleLines())
|
|
||||||
}
|
|
||||||
|
|
||||||
// GotoTop sets the viewport to the top position.
|
|
||||||
func (m *Model) GotoTop() (lines []string) {
|
|
||||||
if m.AtTop() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
m.SetYOffset(0)
|
|
||||||
return m.visibleLines()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GotoBottom sets the viewport to the bottom position.
|
|
||||||
func (m *Model) GotoBottom() (lines []string) {
|
|
||||||
m.SetYOffset(m.maxYOffset())
|
|
||||||
return m.visibleLines()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync tells the renderer where the viewport will be located and requests
|
|
||||||
// a render of the current state of the viewport. It should be called for the
|
|
||||||
// first render and after a window resize.
|
|
||||||
//
|
|
||||||
// For high performance rendering only.
|
|
||||||
//
|
|
||||||
// Deprecated: high performance rendering is deprecated in Bubble Tea.
|
|
||||||
func Sync(m Model) tea.Cmd {
|
|
||||||
if len(m.lines) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
top, bottom := m.scrollArea()
|
|
||||||
return tea.SyncScrollArea(m.visibleLines(), top, bottom)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ViewDown is a high performance command that moves the viewport up by a given
|
|
||||||
// number of lines. Use Model.ViewDown to get the lines that should be rendered.
|
|
||||||
// For example:
|
|
||||||
//
|
|
||||||
// lines := model.ViewDown(1)
|
|
||||||
// cmd := ViewDown(m, lines)
|
|
||||||
//
|
|
||||||
// Deprecated: high performance rendering is deprecated in Bubble Tea.
|
|
||||||
func ViewDown(m Model, lines []string) tea.Cmd {
|
|
||||||
if len(lines) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
top, bottom := m.scrollArea()
|
|
||||||
|
|
||||||
// XXX: high performance rendering is deprecated in Bubble Tea. In a v2 we
|
|
||||||
// won't need to return a command here.
|
|
||||||
return tea.ScrollDown(lines, top, bottom)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ViewUp is a high performance command the moves the viewport down by a given
|
|
||||||
// number of lines height. Use Model.ViewUp to get the lines that should be
|
|
||||||
// rendered.
|
|
||||||
//
|
|
||||||
// Deprecated: high performance rendering is deprecated in Bubble Tea.
|
|
||||||
func ViewUp(m Model, lines []string) tea.Cmd {
|
|
||||||
if len(lines) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
top, bottom := m.scrollArea()
|
|
||||||
|
|
||||||
// XXX: high performance rendering is deprecated in Bubble Tea. In a v2 we
|
|
||||||
// won't need to return a command here.
|
|
||||||
return tea.ScrollUp(lines, top, bottom)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update handles standard message-based viewport updates.
|
|
||||||
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
|
||||||
var cmd tea.Cmd
|
|
||||||
m, cmd = m.updateAsModel(msg)
|
|
||||||
return m, cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// Author's note: this method has been broken out to make it easier to
|
|
||||||
// potentially transition Update to satisfy tea.Model.
|
|
||||||
func (m Model) updateAsModel(msg tea.Msg) (Model, tea.Cmd) {
|
|
||||||
if !m.initialized {
|
|
||||||
m.setInitialValues()
|
|
||||||
}
|
|
||||||
|
|
||||||
var cmd tea.Cmd
|
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case tea.KeyMsg:
|
|
||||||
switch {
|
|
||||||
case key.Matches(msg, m.KeyMap.PageDown):
|
|
||||||
lines := m.PageDown()
|
|
||||||
if m.HighPerformanceRendering {
|
|
||||||
cmd = ViewDown(m, lines)
|
|
||||||
}
|
|
||||||
|
|
||||||
case key.Matches(msg, m.KeyMap.PageUp):
|
|
||||||
lines := m.PageUp()
|
|
||||||
if m.HighPerformanceRendering {
|
|
||||||
cmd = ViewUp(m, lines)
|
|
||||||
}
|
|
||||||
|
|
||||||
case key.Matches(msg, m.KeyMap.HalfPageDown):
|
|
||||||
lines := m.HalfPageDown()
|
|
||||||
if m.HighPerformanceRendering {
|
|
||||||
cmd = ViewDown(m, lines)
|
|
||||||
}
|
|
||||||
|
|
||||||
case key.Matches(msg, m.KeyMap.HalfPageUp):
|
|
||||||
lines := m.HalfPageUp()
|
|
||||||
if m.HighPerformanceRendering {
|
|
||||||
cmd = ViewUp(m, lines)
|
|
||||||
}
|
|
||||||
|
|
||||||
case key.Matches(msg, m.KeyMap.Down):
|
|
||||||
lines := m.ScrollDown(1)
|
|
||||||
if m.HighPerformanceRendering {
|
|
||||||
cmd = ViewDown(m, lines)
|
|
||||||
}
|
|
||||||
|
|
||||||
case key.Matches(msg, m.KeyMap.Up):
|
|
||||||
lines := m.ScrollUp(1)
|
|
||||||
if m.HighPerformanceRendering {
|
|
||||||
cmd = ViewUp(m, lines)
|
|
||||||
}
|
|
||||||
|
|
||||||
case key.Matches(msg, m.KeyMap.Left):
|
|
||||||
m.ScrollLeft(m.horizontalStep)
|
|
||||||
|
|
||||||
case key.Matches(msg, m.KeyMap.Right):
|
|
||||||
m.ScrollRight(m.horizontalStep)
|
|
||||||
}
|
|
||||||
|
|
||||||
case tea.MouseMsg:
|
|
||||||
if !m.MouseWheelEnabled || msg.Action != tea.MouseActionPress {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
switch msg.Button { //nolint:exhaustive
|
|
||||||
case tea.MouseButtonWheelUp:
|
|
||||||
if msg.Shift {
|
|
||||||
// Note that not every terminal emulator sends the shift event for mouse actions by default (looking at you Konsole)
|
|
||||||
m.ScrollLeft(m.horizontalStep)
|
|
||||||
} else {
|
|
||||||
lines := m.ScrollUp(m.MouseWheelDelta)
|
|
||||||
if m.HighPerformanceRendering {
|
|
||||||
cmd = ViewUp(m, lines)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case tea.MouseButtonWheelDown:
|
|
||||||
if msg.Shift {
|
|
||||||
m.ScrollRight(m.horizontalStep)
|
|
||||||
} else {
|
|
||||||
lines := m.ScrollDown(m.MouseWheelDelta)
|
|
||||||
if m.HighPerformanceRendering {
|
|
||||||
cmd = ViewDown(m, lines)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Note that not every terminal emulator sends the horizontal wheel events by default (looking at you Konsole)
|
|
||||||
case tea.MouseButtonWheelLeft:
|
|
||||||
m.ScrollLeft(m.horizontalStep)
|
|
||||||
case tea.MouseButtonWheelRight:
|
|
||||||
m.ScrollRight(m.horizontalStep)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return m, cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// View renders the viewport into a string.
|
|
||||||
func (m Model) View() string {
|
|
||||||
if m.HighPerformanceRendering {
|
|
||||||
// Just send newlines since we're going to be rendering the actual
|
|
||||||
// content separately. We still need to send something that equals the
|
|
||||||
// height of this view so that the Bubble Tea standard renderer can
|
|
||||||
// position anything below this view properly.
|
|
||||||
return strings.Repeat("\n", max(0, m.Height-1))
|
|
||||||
}
|
|
||||||
|
|
||||||
w, h := m.Width, m.Height
|
|
||||||
if sw := m.Style.GetWidth(); sw != 0 {
|
|
||||||
w = min(w, sw)
|
|
||||||
}
|
|
||||||
if sh := m.Style.GetHeight(); sh != 0 {
|
|
||||||
h = min(h, sh)
|
|
||||||
}
|
|
||||||
contentWidth := w - m.Style.GetHorizontalFrameSize()
|
|
||||||
contentHeight := h - m.Style.GetVerticalFrameSize()
|
|
||||||
contents := lipgloss.NewStyle().
|
|
||||||
Width(contentWidth). // pad to width.
|
|
||||||
Height(contentHeight). // pad to height.
|
|
||||||
MaxHeight(contentHeight). // truncate height if taller.
|
|
||||||
MaxWidth(contentWidth). // truncate width if wider.
|
|
||||||
Render(strings.Join(m.visibleLines(), "\n"))
|
|
||||||
return m.Style.
|
|
||||||
UnsetWidth().UnsetHeight(). // Style size already applied in contents.
|
|
||||||
Render(contents)
|
|
||||||
}
|
|
||||||
|
|
||||||
func clamp(v, low, high int) int {
|
|
||||||
if high < low {
|
|
||||||
low, high = high, low
|
|
||||||
}
|
|
||||||
return min(high, max(low, v))
|
|
||||||
}
|
|
||||||
|
|
||||||
func findLongestLineWidth(lines []string) int {
|
|
||||||
w := 0
|
|
||||||
for _, l := range lines {
|
|
||||||
if ww := ansi.StringWidth(l); ww > w {
|
|
||||||
w = ww
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return w
|
|
||||||
}
|
|
||||||
6
vendor/modules.txt
vendored
6
vendor/modules.txt
vendored
@ -65,10 +65,6 @@ github.com/cenkalti/backoff/v5
|
|||||||
# github.com/cespare/xxhash/v2 v2.3.0
|
# github.com/cespare/xxhash/v2 v2.3.0
|
||||||
## explicit; go 1.11
|
## explicit; go 1.11
|
||||||
github.com/cespare/xxhash/v2
|
github.com/cespare/xxhash/v2
|
||||||
# github.com/charmbracelet/bubbles v0.21.0
|
|
||||||
## explicit; go 1.23.0
|
|
||||||
github.com/charmbracelet/bubbles/key
|
|
||||||
github.com/charmbracelet/bubbles/viewport
|
|
||||||
# github.com/charmbracelet/bubbletea v1.3.10
|
# github.com/charmbracelet/bubbletea v1.3.10
|
||||||
## explicit; go 1.24.0
|
## explicit; go 1.24.0
|
||||||
github.com/charmbracelet/bubbletea
|
github.com/charmbracelet/bubbletea
|
||||||
@ -89,6 +85,8 @@ github.com/charmbracelet/x/ansi/parser
|
|||||||
# github.com/charmbracelet/x/cellbuf v0.0.13
|
# github.com/charmbracelet/x/cellbuf v0.0.13
|
||||||
## explicit; go 1.18
|
## explicit; go 1.18
|
||||||
github.com/charmbracelet/x/cellbuf
|
github.com/charmbracelet/x/cellbuf
|
||||||
|
# github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91
|
||||||
|
## explicit; go 1.19
|
||||||
# github.com/charmbracelet/x/term v0.2.1
|
# github.com/charmbracelet/x/term v0.2.1
|
||||||
## explicit; go 1.18
|
## explicit; go 1.18
|
||||||
github.com/charmbracelet/x/term
|
github.com/charmbracelet/x/term
|
||||||
|
|||||||
Reference in New Issue
Block a user