Compare commits
1 Commits
fixBrokenR
...
fix/613
| Author | SHA1 | Date | |
|---|---|---|---|
|
4ef849767f
|
@ -260,7 +260,6 @@ 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)
|
||||||
@ -281,21 +280,13 @@ 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) {
|
||||||
recipeVersions, warnings, err := app.Recipe.GetRecipeVersions()
|
versions, err := app.Recipe.Tags()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, warning := range warnings {
|
if len(versions) > 0 && !internal.Chaos {
|
||||||
log.Warn(warning)
|
return versions[len(versions)-1], nil
|
||||||
}
|
|
||||||
|
|
||||||
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,14 +98,10 @@ var AppNewCommand = &cobra.Command{
|
|||||||
var recipeVersions recipePkg.RecipeVersions
|
var recipeVersions recipePkg.RecipeVersions
|
||||||
if recipeVersion == "" {
|
if recipeVersion == "" {
|
||||||
var err error
|
var err error
|
||||||
var warnings []string
|
recipeVersions, _, err = recipe.GetRecipeVersions()
|
||||||
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 {
|
||||||
@ -114,8 +110,6 @@ 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,7 +128,6 @@ 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,7 +246,6 @@ 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,7 +282,6 @@ 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 := fmt.Sprintf("https://%s", app.Domain)
|
domain := app.Domain
|
||||||
if domain == "" {
|
if domain == "" {
|
||||||
domain = config.MISSING_DEFAULT
|
domain = config.MISSING_DEFAULT
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package recipe
|
package recipe
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -20,9 +19,6 @@ 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")
|
||||||
@ -125,18 +121,20 @@ 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 {
|
||||||
@ -300,14 +298,3 @@ 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
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,33 +0,0 @@
|
|||||||
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,7 +57,9 @@ 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,6 +8,7 @@ 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
|
||||||
@ -41,7 +42,6 @@ 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,6 +133,8 @@ 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,11 +633,6 @@ 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,16 +224,3 @@ 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,27 +37,18 @@ 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 serverName.
|
// For this use-case, please pass "default" as the contextName.
|
||||||
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, 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))
|
return nil, errors.New(i18n.G("server missing context, run \"abra server add %s\"?", serverName))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil, errors.New(i18n.G("unknown server, 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,18 +403,15 @@ 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: %s", tag, r.Dir, err))
|
log.Debug(i18n.G("failed to check out %s in %s", tag, r.Dir))
|
||||||
warnMsg = append(warnMsg, i18n.G("skipping tag %s: checkout failed: %s", tag, err))
|
return 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 {
|
||||||
log.Debug(i18n.G("failed to get compose config for %s: %s", tag, err))
|
return 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)
|
||||||
@ -422,9 +419,7 @@ 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 {
|
||||||
log.Debug(i18n.G("failed to parse image for %s in %s: %s", service.Name, tag, err))
|
return 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)
|
||||||
@ -450,7 +445,6 @@ 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,6 +11,7 @@ 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"
|
||||||
@ -41,6 +42,12 @@ 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
|
||||||
@ -49,6 +56,10 @@ 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
|
||||||
@ -236,7 +247,10 @@ 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 cmds []tea.Cmd
|
var (
|
||||||
|
cmd tea.Cmd
|
||||||
|
cmds []tea.Cmd
|
||||||
|
)
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
@ -244,11 +258,25 @@ 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
|
||||||
@ -256,9 +284,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
|
||||||
@ -318,12 +346,46 @@ 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,7 +201,6 @@ 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"))
|
||||||
@ -227,7 +226,6 @@ func RunDeploy(
|
|||||||
appName,
|
appName,
|
||||||
serverName,
|
serverName,
|
||||||
dontWait,
|
dontWait,
|
||||||
noInput,
|
|
||||||
filters,
|
filters,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -250,7 +248,6 @@ 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)
|
||||||
@ -314,7 +311,6 @@ func deployCompose(
|
|||||||
Services: serviceIDs,
|
Services: serviceIDs,
|
||||||
AppName: appName,
|
AppName: appName,
|
||||||
ServerName: serverName,
|
ServerName: serverName,
|
||||||
NoInput: noInput,
|
|
||||||
Filters: filters,
|
Filters: filters,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -565,7 +561,6 @@ 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
|
||||||
@ -575,13 +570,7 @@ 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.12.0-beta"
|
ABRA_VERSION="0.11.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.12.0-beta"
|
RC_VERSION="0.11.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
|
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks --debug
|
||||||
assert_success
|
assert_success
|
||||||
refute_output --partial "timeout: set to"
|
refute_output --partial "timeout: set to"
|
||||||
}
|
}
|
||||||
@ -554,7 +554,6 @@ 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"
|
||||||
@ -580,25 +579,16 @@ teardown(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
# bats test_tags=slow
|
# bats test_tags=slow
|
||||||
@test "re-deploy updates existing env vars" {
|
@test "manually created server without context bails gracefully" {
|
||||||
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input
|
run mkdir -p "$ABRA_DIR/servers/default2"
|
||||||
assert_success
|
assert_success
|
||||||
|
assert_exists "$ABRA_DIR/servers/default2"
|
||||||
|
|
||||||
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
|
run cp "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" "$ABRA_DIR/servers/default2/$TEST_APP_DOMAIN_2.env"
|
||||||
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
|
|
||||||
assert_success
|
assert_success
|
||||||
assert_output --partial "WITH_COMMENT=foo"
|
assert_exists "$ABRA_DIR/servers/default2/$TEST_APP_DOMAIN_2.env"
|
||||||
|
|
||||||
run sed -i 's/WITH_COMMENT=foo/WITH_COMMENT=bar/g' \
|
run $ABRA app deploy "$TEST_APP_DOMAIN_2" --no-input --no-converge-checks
|
||||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
assert_failure
|
||||||
assert_success
|
assert_output --partial "server missing context"
|
||||||
|
|
||||||
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,13 +68,6 @@ 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,6 +160,23 @@ 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,10 +19,6 @@ 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" {
|
||||||
@ -130,71 +126,3 @@ 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
Normal file
21
vendor/github.com/charmbracelet/bubbles/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.
|
||||||
140
vendor/github.com/charmbracelet/bubbles/key/key.go
generated
vendored
Normal file
140
vendor/github.com/charmbracelet/bubbles/key/key.go
generated
vendored
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
// 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
Normal file
60
vendor/github.com/charmbracelet/bubbles/viewport/keymap.go
generated
vendored
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
// 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
Normal file
544
vendor/github.com/charmbracelet/bubbles/viewport/viewport.go
generated
vendored
Normal file
@ -0,0 +1,544 @@
|
|||||||
|
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,6 +65,10 @@ 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
|
||||||
@ -85,8 +89,6 @@ 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