From 2aa5f6151e3607bf6d221a7f308c214bce03671c Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Mon, 14 Jul 2025 10:16:36 -0300 Subject: [PATCH] fix: non-interactive and context cancellation when SIGINT (#143) * fix: non-interactive and context cancellation when SIGINT * fix: spinner interrupt * refactor: remove weird context value usage * fix: improvements * fix: spinner competing for signal handling * fix: vendoring Signed-off-by: Carlos Alexandro Becker * fix: ctrl+c in raw mode * fix: sigkill can't be handled --------- Signed-off-by: Carlos Alexandro Becker --- .gitignore | 1 + cmd/root.go | 6 +- go.mod | 4 +- go.sum | 4 +- internal/app/app.go | 5 +- internal/format/spinner.go | 50 ++- internal/lsp/transport.go | 41 +-- .../sdk/azidentity/go.work.sum | 60 ---- .../github.com/charmbracelet/fang/README.md | 7 +- vendor/github.com/charmbracelet/fang/fang.go | 119 +++++-- vendor/github.com/charmbracelet/fang/help.go | 298 ++++++++++++------ vendor/github.com/charmbracelet/fang/theme.go | 52 ++- vendor/modules.txt | 6 +- 13 files changed, 416 insertions(+), 237 deletions(-) delete mode 100644 vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/go.work.sum diff --git a/.gitignore b/.gitignore index b28e5a0c727163e8f3585522e680d1df2ad6e621..2f16f744432d89e0a72fd6ea8e359678a64b6d42 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ # Go workspace file go.work +go.work.sum # IDE specific files .idea/ diff --git a/cmd/root.go b/cmd/root.go index 3a8f4fba0fe759a42ef1e7647223b2b3b11fbc65..c5231accd672c43a564e4d0a174fb762d06ee044 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -6,6 +6,7 @@ import ( "io" "log/slog" "os" + "syscall" "time" tea "github.com/charmbracelet/bubbletea/v2" @@ -72,9 +73,7 @@ to assist developers in writing, debugging, and understanding code directly from return err } - // Create main context for the application - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := cmd.Context() // Connect DB, this will also run migrations conn, err := db.Connect(ctx, cfg.Options.DataDirectory) @@ -145,6 +144,7 @@ func Execute() { context.Background(), rootCmd, fang.WithVersion(version.Version), + fang.WithNotifySignal(os.Interrupt, syscall.SIGTERM), ); err != nil { os.Exit(1) } diff --git a/go.mod b/go.mod index 2a9d6d5dfbaa827a5c8a57cadbe716dd956e1401..d510a774a03c27ceca623400257228763cc2e9a1 100644 --- a/go.mod +++ b/go.mod @@ -18,9 +18,9 @@ require ( github.com/charlievieth/fastwalk v1.0.11 github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250710161907-a4c42b579198 github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.1 - github.com/charmbracelet/fang v0.1.0 + github.com/charmbracelet/fang v0.3.1-0.20250711140230-d5ebb8c1d674 github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe - github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.2.0.20250703152125-8e1c474f8a71 + github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3 github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706 github.com/charmbracelet/x/ansi v0.9.3 github.com/charmbracelet/x/exp/charmtone v0.0.0-20250708181618-a60a724ba6c3 diff --git a/go.sum b/go.sum index 1d40961a3dce4180d9a06d17e3843f8d8709567b..d7004401154b86ce0658162c06bfc610a0c77126 100644 --- a/go.sum +++ b/go.sum @@ -74,8 +74,8 @@ github.com/charmbracelet/bubbletea-internal/v2 v2.0.0-20250710185017-3c0ffd25e59 github.com/charmbracelet/bubbletea-internal/v2 v2.0.0-20250710185017-3c0ffd25e595/go.mod h1:+Tl7rePElw6OKt382t04zXwtPFoPXxAaJzNrYmtsLds= github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40= github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0= -github.com/charmbracelet/fang v0.1.0 h1:SlZS2crf3/zQh7Mr4+W+7QR1k+L08rrPX5rm5z3d7Wg= -github.com/charmbracelet/fang v0.1.0/go.mod h1:Zl/zeUQ8EtQuGyiV0ZKZlZPDowKRTzu8s/367EpN/fc= +github.com/charmbracelet/fang v0.3.1-0.20250711140230-d5ebb8c1d674 h1:+Cz+VfxD5DO+JT1LlswXWhre0HYLj6l2HW8HVGfMuC0= +github.com/charmbracelet/fang v0.3.1-0.20250711140230-d5ebb8c1d674/go.mod h1:9gCUAHmVx5BwSafeyNr3GI0GgvlB1WYjL21SkPp1jyU= github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe h1:i6ce4CcAlPpTj2ER69m1DBeLZ3RRcHnKExuwhKa3GfY= github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe/go.mod h1:p3Q+aN4eQKeM5jhrmXPMgPrlKbmc59rWSnMsSA3udhk= github.com/charmbracelet/lipgloss-internal/v2 v2.0.0-20250710185058-03664cb9cecb h1:lswj7CYZVYbLn2OhYJsXOMRQQGdRIfyuSnh5FdVSMr0= diff --git a/internal/app/app.go b/internal/app/app.go index 9d0e6f176b14df0b15fd90f4b3651cdefafd6826..c3dae3d88a2be7c4cd5491e089b97695b08a7a23 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -95,10 +95,13 @@ func New(ctx context.Context, conn *sql.DB, cfg *config.Config) (*App, error) { func (a *App) RunNonInteractive(ctx context.Context, prompt string, quiet bool) error { slog.Info("Running in non-interactive mode") + ctx, cancel := context.WithCancel(ctx) + defer cancel() + // Start spinner if not in quiet mode var spinner *format.Spinner if !quiet { - spinner = format.NewSpinner(ctx, "Generating") + spinner = format.NewSpinner(ctx, cancel, "Generating") spinner.Start() } // Helper function to stop spinner once diff --git a/internal/format/spinner.go b/internal/format/spinner.go index 9377bd3b4c145fc6866ac1e0f4e63dff8ab51619..da64fb93ce262e04a0b5fb9da8c4aea8403d10d8 100644 --- a/internal/format/spinner.go +++ b/internal/format/spinner.go @@ -18,24 +18,48 @@ type Spinner struct { prog *tea.Program } +type model struct { + cancel context.CancelFunc + anim anim.Anim +} + +func (m model) Init() tea.Cmd { return m.anim.Init() } +func (m model) View() string { return m.anim.View() } + +// Update implements tea.Model. +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyPressMsg: + switch msg.String() { + case "ctrl+c", "esc": + m.cancel() + return m, tea.Quit + } + } + mm, cmd := m.anim.Update(msg) + m.anim = mm.(anim.Anim) + return m, cmd +} + // NewSpinner creates a new spinner with the given message -func NewSpinner(ctx context.Context, message string) *Spinner { +func NewSpinner(ctx context.Context, cancel context.CancelFunc, message string) *Spinner { t := styles.CurrentTheme() - model := anim.New(anim.Settings{ - Size: 10, - Label: message, - LabelColor: t.FgBase, - GradColorA: t.Primary, - GradColorB: t.Secondary, - CycleColors: true, - }) + model := model{ + anim: anim.New(anim.Settings{ + Size: 10, + Label: message, + LabelColor: t.FgBase, + GradColorA: t.Primary, + GradColorB: t.Secondary, + CycleColors: true, + }), + cancel: cancel, + } prog := tea.NewProgram( model, - tea.WithInput(nil), tea.WithOutput(os.Stderr), tea.WithContext(ctx), - tea.WithoutCatchPanics(), ) return &Spinner{ @@ -47,13 +71,13 @@ func NewSpinner(ctx context.Context, message string) *Spinner { // Start begins the spinner animation func (s *Spinner) Start() { go func() { + defer close(s.done) _, err := s.prog.Run() // ensures line is cleared fmt.Fprint(os.Stderr, ansi.EraseEntireLine) - if err != nil && !errors.Is(err, context.Canceled) { + if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, tea.ErrInterrupted) { fmt.Fprintf(os.Stderr, "Error running spinner: %v\n", err) } - close(s.done) }() } diff --git a/internal/lsp/transport.go b/internal/lsp/transport.go index 431a099fa1cda5e5035de7ce6c10ef3761e397ea..9a3dfd261fb68b1afdd17f614daab761f9294327 100644 --- a/internal/lsp/transport.go +++ b/internal/lsp/transport.go @@ -222,29 +222,32 @@ func (c *Client) Call(ctx context.Context, method string, params any, result any } // Wait for response - resp := <-ch - - if cfg.Options.DebugLSP { - slog.Debug("Received response", "id", id) - } - - if resp.Error != nil { - return fmt.Errorf("request failed: %s (code: %d)", resp.Error.Message, resp.Error.Code) - } + select { + case <-ctx.Done(): + return ctx.Err() + case resp := <-ch: + if cfg.Options.DebugLSP { + slog.Debug("Received response", "id", id) + } - if result != nil { - // If result is a json.RawMessage, just copy the raw bytes - if rawMsg, ok := result.(*json.RawMessage); ok { - *rawMsg = resp.Result - return nil + if resp.Error != nil { + return fmt.Errorf("request failed: %s (code: %d)", resp.Error.Message, resp.Error.Code) } - // Otherwise unmarshal into the provided type - if err := json.Unmarshal(resp.Result, result); err != nil { - return fmt.Errorf("failed to unmarshal result: %w", err) + + if result != nil { + // If result is a json.RawMessage, just copy the raw bytes + if rawMsg, ok := result.(*json.RawMessage); ok { + *rawMsg = resp.Result + return nil + } + // Otherwise unmarshal into the provided type + if err := json.Unmarshal(resp.Result, result); err != nil { + return fmt.Errorf("failed to unmarshal result: %w", err) + } } - } - return nil + return nil + } } // Notify sends a notification (a request without an ID that doesn't expect a response) diff --git a/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/go.work.sum b/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/go.work.sum deleted file mode 100644 index c592f283b6bdb1cb2b13aa4b0769b94811a1cfe9..0000000000000000000000000000000000000000 --- a/vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/go.work.sum +++ /dev/null @@ -1,60 +0,0 @@ -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0-beta.1 h1:ODs3brnqQM99Tq1PffODpAViYv3Bf8zOg464MU7p5ew= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0-beta.1/go.mod h1:3Ug6Qzto9anB6mGlEdgYMDF5zHQ+wwhEaYR4s17PHMw= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0 h1:fb8kj/Dh4CSwgsOzHeZY4Xh68cFVbzXx+ONXGMY//4w= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0/go.mod h1:uReU2sSxZExRPBAg3qKzmAucSi51+SP1OhohieR821Q= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/keybase/dbus v0.0.0-20220506165403-5aa21ea2c23a/go.mod h1:YPNKjjE7Ubp9dTbnWvsP3HT+hYnY6TfXzubYTBeUxc8= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= -golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= -golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/vendor/github.com/charmbracelet/fang/README.md b/vendor/github.com/charmbracelet/fang/README.md index 88a225cfd6e698d15dd29a9af0a5dca74b61ecf7..575b40ce13fa57eb0e41082943a3c21e05c82777 100644 --- a/vendor/github.com/charmbracelet/fang/README.md +++ b/vendor/github.com/charmbracelet/fang/README.md @@ -1,7 +1,7 @@ # Fang

- Charm Fang + Charm Fang

Latest Release @@ -12,7 +12,7 @@ The CLI starter kit. A small, experimental library for batteries-included [Cobra][cobra] applications.

- fang-02 + The Charm Fang mascot and title treatment

## Features @@ -45,6 +45,7 @@ To use it, invoke `fang.Execute` passing your root `*cobra.Command`: package main import ( + "context" "os" "github.com/charmbracelet/fang" @@ -56,7 +57,7 @@ func main() { Use: "example", Short: "A simple example program!", } - if err := fang.Execute(context.TODO(), cmd); err != nil { + if err := fang.Execute(context.Background(), cmd); err != nil { os.Exit(1) } } diff --git a/vendor/github.com/charmbracelet/fang/fang.go b/vendor/github.com/charmbracelet/fang/fang.go index c1f9bc06a5299c991bac569aa6868e3d08fcd37c..6a6ab99a63fc4debf404694473d23e0a576d2fab 100644 --- a/vendor/github.com/charmbracelet/fang/fang.go +++ b/vendor/github.com/charmbracelet/fang/fang.go @@ -4,11 +4,14 @@ package fang import ( "context" "fmt" + "io" "os" + "os/signal" "runtime/debug" "github.com/charmbracelet/colorprofile" "github.com/charmbracelet/lipgloss/v2" + "github.com/charmbracelet/x/term" mango "github.com/muesli/mango-cobra" "github.com/muesli/roff" "github.com/spf13/cobra" @@ -16,12 +19,24 @@ import ( const shaLen = 7 +// ErrorHandler handles an error, printing them to the given [io.Writer]. +// +// Note that this will only be used if the STDERR is a terminal, and should +// be used for styling only. +type ErrorHandler = func(w io.Writer, styles Styles, err error) + +// ColorSchemeFunc gets a [lipgloss.LightDarkFunc] and returns a [ColorScheme]. +type ColorSchemeFunc = func(lipgloss.LightDarkFunc) ColorScheme + type settings struct { completions bool manpages bool + skipVersion bool version string commit string - theme *ColorScheme + colorscheme ColorSchemeFunc + errHandler ErrorHandler + signals []os.Signal } // Option changes fang settings. @@ -41,10 +56,21 @@ func WithoutManpage() Option { } } +// WithColorSchemeFunc sets a function that return colorscheme. +func WithColorSchemeFunc(cs ColorSchemeFunc) Option { + return func(s *settings) { + s.colorscheme = cs + } +} + // WithTheme sets the colorscheme. +// +// Deprecated: use [WithColorSchemeFunc] instead. func WithTheme(theme ColorScheme) Option { return func(s *settings) { - s.theme = &theme + s.colorscheme = func(lipgloss.LightDarkFunc) ColorScheme { + return theme + } } } @@ -55,6 +81,13 @@ func WithVersion(version string) Option { } } +// WithoutVersion skips the `-v`/`--version` functionality. +func WithoutVersion() Option { + return func(s *settings) { + s.skipVersion = true + } +} + // WithCommit sets the commit SHA. func WithCommit(commit string) Option { return func(s *settings) { @@ -62,30 +95,45 @@ func WithCommit(commit string) Option { } } +// WithErrorHandler sets the error handler. +func WithErrorHandler(handler ErrorHandler) Option { + return func(s *settings) { + s.errHandler = handler + } +} + +// WithNotifySignal sets the signals that should interrupt the execution of the +// program. +func WithNotifySignal(signals ...os.Signal) Option { + return func(s *settings) { + s.signals = signals + } +} + // Execute applies fang to the command and executes it. func Execute(ctx context.Context, root *cobra.Command, options ...Option) error { opts := settings{ manpages: true, completions: true, + colorscheme: DefaultColorScheme, + errHandler: DefaultErrorHandler, } + for _, option := range options { option(&opts) } - if opts.theme == nil { - isDark := lipgloss.HasDarkBackground(os.Stdin, os.Stderr) - t := DefaultTheme(isDark) - opts.theme = &t + helpFunc := func(c *cobra.Command, _ []string) { + w := colorprofile.NewWriter(c.OutOrStdout(), os.Environ()) + helpFn(c, w, makeStyles(mustColorscheme(opts.colorscheme))) } - styles := makeStyles(*opts.theme) - - root.SetHelpFunc(func(c *cobra.Command, _ []string) { - w := colorprofile.NewWriter(c.OutOrStdout(), os.Environ()) - helpFn(c, w, styles) - }) root.SilenceUsage = true root.SilenceErrors = true + if !opts.skipVersion { + root.Version = buildVersion(opts) + } + root.SetHelpFunc(helpFunc) if opts.manpages { root.AddCommand(&cobra.Command{ @@ -108,34 +156,49 @@ func Execute(ctx context.Context, root *cobra.Command, options ...Option) error }) } - if opts.completions { - root.InitDefaultCompletionCmd() - } else { + if !opts.completions { root.CompletionOptions.DisableDefaultCmd = true } - if opts.version == "" { - if info, ok := debug.ReadBuildInfo(); ok && info.Main.Sum != "" { - opts.version = info.Main.Version - opts.commit = getKey(info, "vcs.revision") - } else { - opts.version = "unknown (built from source)" - } - } - if len(opts.commit) >= shaLen { - opts.version += " (" + opts.commit[:shaLen] + ")" + if len(opts.signals) > 0 { + var cancel context.CancelFunc + ctx, cancel = signal.NotifyContext(ctx, opts.signals...) + defer cancel() } - root.Version = opts.version - if err := root.ExecuteContext(ctx); err != nil { + if w, ok := root.ErrOrStderr().(term.File); ok { + // if stderr is not a tty, simply print the error without any + // styling or going through an [ErrorHandler]: + if !term.IsTerminal(w.Fd()) { + _, _ = fmt.Fprintln(w, err.Error()) + return err //nolint:wrapcheck + } + } w := colorprofile.NewWriter(root.ErrOrStderr(), os.Environ()) - writeError(w, styles, err) + opts.errHandler(w, makeStyles(mustColorscheme(opts.colorscheme)), err) return err //nolint:wrapcheck } return nil } +func buildVersion(opts settings) string { + commit := opts.commit + version := opts.version + if version == "" { + if info, ok := debug.ReadBuildInfo(); ok && info.Main.Sum != "" { + version = info.Main.Version + commit = getKey(info, "vcs.revision") + } else { + version = "unknown (built from source)" + } + } + if len(commit) >= shaLen { + version += " (" + commit[:shaLen] + ")" + } + return version +} + func getKey(info *debug.BuildInfo, key string) string { if info == nil { return "" diff --git a/vendor/github.com/charmbracelet/fang/help.go b/vendor/github.com/charmbracelet/fang/help.go index 340090eadf1f779c0e702b03440d7e7efb29b62b..ba2a6185844787e83753c51c3415d5ccc06e36ec 100644 --- a/vendor/github.com/charmbracelet/fang/help.go +++ b/vendor/github.com/charmbracelet/fang/help.go @@ -3,7 +3,10 @@ package fang import ( "cmp" "fmt" + "io" + "iter" "os" + "reflect" "regexp" "strconv" "strings" @@ -20,6 +23,7 @@ import ( const ( minSpace = 10 shortPad = 2 + longPad = 4 ) var width = sync.OnceValue(func() int { @@ -45,65 +49,95 @@ func helpFn(c *cobra.Command, w *colorprofile.Writer, styles Styles) { blockWidth = max(blockWidth, lipgloss.Width(ex)) } blockWidth = min(width()-padding, blockWidth+padding) + blockStyle := styles.Codeblock.Base.Width(blockWidth) - styles.Codeblock.Base = styles.Codeblock.Base.Width(blockWidth) + // if the color profile is ascii or notty, or if the block has no + // background color set, remove the vertical padding. + if w.Profile <= colorprofile.Ascii || reflect.DeepEqual(blockStyle.GetBackground(), lipgloss.NoColor{}) { + blockStyle = blockStyle.PaddingTop(0).PaddingBottom(0) + } _, _ = fmt.Fprintln(w, styles.Title.Render("usage")) - _, _ = fmt.Fprintln(w, styles.Codeblock.Base.Render(usage)) + _, _ = fmt.Fprintln(w, blockStyle.Render(usage)) if len(examples) > 0 { - cw := styles.Codeblock.Base.GetWidth() - styles.Codeblock.Base.GetHorizontalPadding() + cw := blockStyle.GetWidth() - blockStyle.GetHorizontalPadding() _, _ = fmt.Fprintln(w, styles.Title.Render("examples")) for i, example := range examples { if lipgloss.Width(example) > cw { examples[i] = ansi.Truncate(example, cw, "…") } } - _, _ = fmt.Fprintln(w, styles.Codeblock.Base.Render(strings.Join(examples, "\n"))) + _, _ = fmt.Fprintln(w, blockStyle.Render(strings.Join(examples, "\n"))) } + groups, groupKeys := evalGroups(c) cmds, cmdKeys := evalCmds(c, styles) flags, flagKeys := evalFlags(c, styles) space := calculateSpace(cmdKeys, flagKeys) - leftPadding := 4 - if len(cmds) > 0 { - _, _ = fmt.Fprintln(w, styles.Title.Render("commands")) - for _, k := range cmdKeys { - _, _ = fmt.Fprintln(w, lipgloss.JoinHorizontal( - lipgloss.Left, - lipgloss.NewStyle().PaddingLeft(leftPadding).Render(k), - strings.Repeat(" ", space-lipgloss.Width(k)), - cmds[k], - )) + for _, groupID := range groupKeys { + group := cmds[groupID] + if len(group) == 0 { + continue } + renderGroup(w, styles, space, groups[groupID], func(yield func(string, string) bool) { + for _, k := range cmdKeys { + cmds, ok := group[k] + if !ok { + continue + } + if !yield(k, cmds) { + return + } + } + }) } if len(flags) > 0 { - _, _ = fmt.Fprintln(w, styles.Title.Render("flags")) - for _, k := range flagKeys { - _, _ = fmt.Fprintln(w, lipgloss.JoinHorizontal( - lipgloss.Left, - lipgloss.NewStyle().PaddingLeft(leftPadding).Render(k), - strings.Repeat(" ", space-lipgloss.Width(k)), - flags[k], - )) - } + renderGroup(w, styles, space, "flags", func(yield func(string, string) bool) { + for _, k := range flagKeys { + if !yield(k, flags[k]) { + return + } + } + }) } _, _ = fmt.Fprintln(w) } -func writeError(w *colorprofile.Writer, styles Styles, err error) { +// DefaultErrorHandler is the default [ErrorHandler] implementation. +func DefaultErrorHandler(w io.Writer, styles Styles, err error) { _, _ = fmt.Fprintln(w, styles.ErrorHeader.String()) _, _ = fmt.Fprintln(w, styles.ErrorText.Render(err.Error()+".")) _, _ = fmt.Fprintln(w) - _, _ = fmt.Fprintln(w, lipgloss.JoinHorizontal( - lipgloss.Left, - styles.ErrorText.UnsetWidth().Render("Try"), - styles.Program.Flag.Render("--help"), - styles.ErrorText.UnsetWidth().UnsetMargins().UnsetTransform().PaddingLeft(1).Render("for usage."), - )) - _, _ = fmt.Fprintln(w) + if isUsageError(err) { + _, _ = fmt.Fprintln(w, lipgloss.JoinHorizontal( + lipgloss.Left, + styles.ErrorText.UnsetWidth().Render("Try"), + styles.Program.Flag.Render(" --help "), + styles.ErrorText.UnsetWidth().UnsetMargins().UnsetTransform().Render("for usage."), + )) + _, _ = fmt.Fprintln(w) + } +} + +// XXX: this is a hack to detect usage errors. +// See: https://github.com/spf13/cobra/pull/2266 +func isUsageError(err error) bool { + s := err.Error() + for _, prefix := range []string{ + "flag needs an argument:", + "unknown flag:", + "unknown shorthand flag:", + "unknown command", + "invalid argument", + } { + if strings.HasPrefix(s, prefix) { + return true + } + } + return false } func writeLongShort(w *colorprofile.Writer, styles Styles, longShort string) { @@ -118,8 +152,10 @@ var otherArgsRe = regexp.MustCompile(`(\[.*\])`) // styleUsage stylized styleUsage line for a given command. func styleUsage(c *cobra.Command, styles Program, complete bool) string { - // XXX: maybe use c.UseLine() here? u := c.Use + if complete { + u = c.UseLine() + } hasArgs := strings.Contains(u, "[args]") hasFlags := strings.Contains(u, "[flags]") || strings.Contains(u, "[--flags]") || c.HasFlags() || c.HasPersistentFlags() || c.HasAvailableFlags() hasCommands := strings.Contains(u, "[command]") || c.HasAvailableSubCommands() @@ -139,34 +175,38 @@ func styleUsage(c *cobra.Command, styles Program, complete bool) string { u = strings.TrimSpace(u) - useLine := []string{ - styles.Name.Render(u), - } - if !complete { - useLine[0] = styles.Command.Render(u) + useLine := []string{} + if complete { + parts := strings.Fields(u) + useLine = append(useLine, styles.Name.Render(parts[0])) + if len(parts) > 1 { + useLine = append(useLine, styles.Command.Render(" "+strings.Join(parts[1:], " "))) + } + } else { + useLine = append(useLine, styles.Command.Render(u)) } if hasCommands { useLine = append( useLine, - styles.DimmedArgument.Render("[command]"), + styles.DimmedArgument.Render(" [command]"), ) } if hasArgs { useLine = append( useLine, - styles.DimmedArgument.Render("[args]"), + styles.DimmedArgument.Render(" [args]"), ) } for _, arg := range otherArgs { useLine = append( useLine, - styles.DimmedArgument.Render(arg), + styles.DimmedArgument.Render(" "+arg), ) } if hasFlags { useLine = append( useLine, - styles.DimmedArgument.Render("[--flags]"), + styles.DimmedArgument.Render(" [--flags]"), ) } return lipgloss.JoinHorizontal(lipgloss.Left, useLine...) @@ -180,19 +220,21 @@ func styleExamples(c *cobra.Command, styles Styles) []string { } usage := []string{} examples := strings.Split(c.Example, "\n") + var indent bool for i, line := range examples { line = strings.TrimSpace(line) if (i == 0 || i == len(examples)-1) && line == "" { continue } - s := styleExample(c, line, styles.Codeblock) + s := styleExample(c, line, indent, styles.Codeblock) usage = append(usage, s) + indent = len(line) > 1 && (line[len(line)-1] == '\\' || line[len(line)-1] == '|') } return usage } -func styleExample(c *cobra.Command, line string, styles Codeblock) string { +func styleExample(c *cobra.Command, line string, indent bool, styles Codeblock) string { if strings.HasPrefix(line, "# ") { return lipgloss.JoinHorizontal( lipgloss.Left, @@ -200,66 +242,110 @@ func styleExample(c *cobra.Command, line string, styles Codeblock) string { ) } - args := strings.Fields(line) - var nextIsFlag bool var isQuotedString bool + var foundProgramName bool + var isRedirecting bool + programName := c.Root().Name() + args := strings.Fields(line) + var cleanArgs []string for i, arg := range args { - if i == 0 { - args[i] = styles.Program.Name.Render(arg) - continue + isQuoteStart := arg[0] == '"' || arg[0] == '\'' + isQuoteEnd := arg[len(arg)-1] == '"' || arg[len(arg)-1] == '\'' + isFlag := arg[0] == '-' + + switch i { + case 0: + args[i] = "" + if indent { + args[i] = styles.Program.DimmedArgument.Render(" ") + indent = false + } + default: + args[i] = styles.Program.DimmedArgument.Render(" ") } - quoteStart := arg[0] == '"' - quoteEnd := arg[len(arg)-1] == '"' - flagStart := arg[0] == '-' - if i == 1 && !quoteStart && !flagStart { - args[i] = styles.Program.Command.Render(arg) + if isRedirecting { + args[i] += styles.Program.DimmedArgument.Render(arg) + isRedirecting = false continue } - if quoteStart { - isQuotedString = true - } - if isQuotedString { - args[i] = styles.Program.QuotedString.Render(arg) - if quoteEnd { - isQuotedString = false + + switch arg { + case "\\": + if i == len(args)-1 { + args[i] += styles.Program.DimmedArgument.Render(arg) + continue } + case "|", "||", "-", "&", "&&": + args[i] += styles.Program.DimmedArgument.Render(arg) continue } - if nextIsFlag { - args[i] = styles.Program.Flag.Render(arg) + + if isRedirect(arg) { + args[i] += styles.Program.DimmedArgument.Render(arg) + isRedirecting = true continue } - var dashes string - if strings.HasPrefix(arg, "-") { - dashes = "-" + + if !foundProgramName { //nolint:nestif + if isQuotedString { + args[i] += styles.Program.QuotedString.Render(arg) + isQuotedString = !isQuoteEnd + continue + } + if left, right, ok := strings.Cut(arg, "="); ok { + args[i] += styles.Program.Flag.Render(left + "=") + if right[0] == '"' { + isQuotedString = true + args[i] += styles.Program.QuotedString.Render(right) + continue + } + args[i] += styles.Program.Argument.Render(right) + continue + } + + if arg == programName { + args[i] += styles.Program.Name.Render(arg) + foundProgramName = true + continue + } } - if strings.HasPrefix(arg, "--") { - dashes = "--" + + if !isQuoteStart && !isQuotedString && !isFlag { + cleanArgs = append(cleanArgs, arg) + } + + if !isQuoteStart && !isFlag && isSubCommand(c, cleanArgs, arg) { + args[i] += styles.Program.Command.Render(arg) + continue + } + isQuotedString = isQuotedString || isQuoteStart + if isQuotedString { + args[i] += styles.Program.QuotedString.Render(arg) + isQuotedString = !isQuoteEnd + continue } // handle a flag - if dashes != "" { + if isFlag { name, value, ok := strings.Cut(arg, "=") - name = strings.TrimPrefix(name, dashes) // it is --flag=value if ok { - args[i] = lipgloss.JoinHorizontal( + args[i] += lipgloss.JoinHorizontal( lipgloss.Left, - styles.Program.Flag.Render(dashes+name+"="), - styles.Program.Argument.UnsetPadding().Render(value), + styles.Program.Flag.Render(name+"="), + styles.Program.Argument.Render(value), ) continue } // it is either --bool-flag or --flag value - args[i] = lipgloss.JoinHorizontal( + args[i] += lipgloss.JoinHorizontal( lipgloss.Left, - styles.Program.Flag.Render(dashes+name), + styles.Program.Flag.Render(name), ) - // if the flag is not a bool flag, next arg continues current flag - nextIsFlag = !isFlagBool(c, name) continue } - args[i] = styles.Program.Argument.Render(arg) + + args[i] += styles.Program.Argument.Render(arg) } return lipgloss.JoinHorizontal( @@ -284,8 +370,7 @@ func evalFlags(c *cobra.Command, styles Styles) (map[string]string, []string) { } else { parts = append( parts, - styles.Program.Flag.Render("-"+f.Shorthand), - styles.Program.Flag.Render("--"+f.Name), + styles.Program.Flag.Render("-"+f.Shorthand+" --"+f.Name), ) } key := lipgloss.JoinHorizontal(lipgloss.Left, parts...) @@ -303,22 +388,50 @@ func evalFlags(c *cobra.Command, styles Styles) (map[string]string, []string) { return flags, keys } -func evalCmds(c *cobra.Command, styles Styles) (map[string]string, []string) { +// result is map[groupID]map[styled cmd name]styled cmd help, and the keys in +// the order they are defined. +func evalCmds(c *cobra.Command, styles Styles) (map[string](map[string]string), []string) { padStyle := lipgloss.NewStyle().PaddingLeft(0) //nolint:mnd keys := []string{} - cmds := map[string]string{} + cmds := map[string]map[string]string{} for _, sc := range c.Commands() { if sc.Hidden { continue } + if _, ok := cmds[sc.GroupID]; !ok { + cmds[sc.GroupID] = map[string]string{} + } key := padStyle.Render(styleUsage(sc, styles.Program, false)) help := styles.FlagDescription.Render(sc.Short) - cmds[key] = help + cmds[sc.GroupID][key] = help keys = append(keys, key) } return cmds, keys } +func evalGroups(c *cobra.Command) (map[string]string, []string) { + // make sure the default group is the first + ids := []string{""} + groups := map[string]string{"": "commands"} + for _, g := range c.Groups() { + groups[g.ID] = g.Title + ids = append(ids, g.ID) + } + return groups, ids +} + +func renderGroup(w io.Writer, styles Styles, space int, name string, items iter.Seq2[string, string]) { + _, _ = fmt.Fprintln(w, styles.Title.Render(name)) + for key, help := range items { + _, _ = fmt.Fprintln(w, lipgloss.JoinHorizontal( + lipgloss.Left, + lipgloss.NewStyle().PaddingLeft(longPad).Render(key), + strings.Repeat(" ", space-lipgloss.Width(key)), + help, + )) + } +} + func calculateSpace(k1, k2 []string) int { const spaceBetween = 2 space := minSpace @@ -328,13 +441,18 @@ func calculateSpace(k1, k2 []string) int { return space } -func isFlagBool(c *cobra.Command, name string) bool { - flag := c.Flags().Lookup(name) - if flag == nil && len(name) == 1 { - flag = c.Flags().ShorthandLookup(name) - } - if flag == nil { - return false +func isSubCommand(c *cobra.Command, args []string, word string) bool { + cmd, _, _ := c.Root().Traverse(args) + return cmd != nil && cmd.Name() == word +} + +var redirectPrefixes = []string{">", "<", "&>", "2>", "1>", ">>", "2>>"} + +func isRedirect(s string) bool { + for _, p := range redirectPrefixes { + if strings.HasPrefix(s, p) { + return true + } } - return flag.Value.Type() == "bool" + return false } diff --git a/vendor/github.com/charmbracelet/fang/theme.go b/vendor/github.com/charmbracelet/fang/theme.go index 8e3389f6e84b4cc66ed0369f2425c4cc7c27d1b4..12cc868089d475d397691e757f55614a4614e44d 100644 --- a/vendor/github.com/charmbracelet/fang/theme.go +++ b/vendor/github.com/charmbracelet/fang/theme.go @@ -2,10 +2,12 @@ package fang import ( "image/color" + "os" "strings" "github.com/charmbracelet/lipgloss/v2" "github.com/charmbracelet/x/exp/charmtone" + "github.com/charmbracelet/x/term" "golang.org/x/text/cases" "golang.org/x/text/language" ) @@ -31,8 +33,14 @@ type ColorScheme struct { } // DefaultTheme is the default colorscheme. +// +// Deprecated: use [DefaultColorScheme] instead. func DefaultTheme(isDark bool) ColorScheme { - c := lipgloss.LightDark(isDark) + return DefaultColorScheme(lipgloss.LightDark(isDark)) +} + +// DefaultColorScheme is the default colorscheme. +func DefaultColorScheme(c lipgloss.LightDarkFunc) ColorScheme { return ColorScheme{ Base: c(charmtone.Charcoal, charmtone.Ash), Title: charmtone.Charple, @@ -45,7 +53,7 @@ func DefaultTheme(isDark bool) ColorScheme { Argument: c(charmtone.Charcoal, charmtone.Ash), Description: c(charmtone.Charcoal, charmtone.Ash), // flag and command descriptions FlagDefault: c(charmtone.Smoke, charmtone.Squid), // flag default values in descriptions - QuotedString: c(charmtone.Charcoal, charmtone.Ash), + QuotedString: c(charmtone.Coral, charmtone.Salmon), ErrorHeader: [2]color.Color{ charmtone.Butter, charmtone.Cherry, @@ -53,6 +61,26 @@ func DefaultTheme(isDark bool) ColorScheme { } } +// AnsiColorScheme is a ANSI colorscheme. +func AnsiColorScheme(c lipgloss.LightDarkFunc) ColorScheme { + base := c(lipgloss.Black, lipgloss.White) + return ColorScheme{ + Base: base, + Title: lipgloss.Blue, + Description: base, + Comment: c(lipgloss.BrightWhite, lipgloss.BrightBlack), + Flag: lipgloss.Magenta, + FlagDefault: lipgloss.BrightMagenta, + Command: lipgloss.Cyan, + QuotedString: lipgloss.Green, + Argument: base, + Help: base, + Dash: base, + ErrorHeader: [2]color.Color{lipgloss.Black, lipgloss.Red}, + ErrorDetails: lipgloss.Red, + } +} + // Styles represents all the styles used. type Styles struct { Text lipgloss.Style @@ -84,6 +112,14 @@ type Program struct { QuotedString lipgloss.Style } +func mustColorscheme(cs func(lipgloss.LightDarkFunc) ColorScheme) ColorScheme { + var isDark bool + if term.IsTerminal(os.Stdout.Fd()) { + isDark = lipgloss.HasDarkBackground(os.Stdin, os.Stdout) + } + return cs(lipgloss.LightDark(isDark)) +} + func makeStyles(cs ColorScheme) Styles { //nolint:mnd return Styles{ @@ -98,8 +134,7 @@ func makeStyles(cs ColorScheme) Styles { Foreground(cs.Description). Transform(titleFirstWord), FlagDefault: lipgloss.NewStyle(). - Foreground(cs.FlagDefault). - PaddingLeft(1), + Foreground(cs.FlagDefault), Codeblock: Codeblock{ Base: lipgloss.NewStyle(). Background(cs.Codeblock). @@ -116,23 +151,18 @@ func makeStyles(cs ColorScheme) Styles { Background(cs.Codeblock). Foreground(cs.Program), Flag: lipgloss.NewStyle(). - PaddingLeft(1). Background(cs.Codeblock). Foreground(cs.Flag), Argument: lipgloss.NewStyle(). - PaddingLeft(1). Background(cs.Codeblock). Foreground(cs.Argument), DimmedArgument: lipgloss.NewStyle(). - PaddingLeft(1). Background(cs.Codeblock). Foreground(cs.DimmedArgument), Command: lipgloss.NewStyle(). - PaddingLeft(1). Background(cs.Codeblock). Foreground(cs.Command), QuotedString: lipgloss.NewStyle(). - PaddingLeft(1). Background(cs.Codeblock). Foreground(cs.QuotedString), }, @@ -141,18 +171,14 @@ func makeStyles(cs ColorScheme) Styles { Name: lipgloss.NewStyle(). Foreground(cs.Program), Argument: lipgloss.NewStyle(). - PaddingLeft(1). Foreground(cs.Argument), DimmedArgument: lipgloss.NewStyle(). - PaddingLeft(1). Foreground(cs.DimmedArgument), Flag: lipgloss.NewStyle(). - PaddingLeft(1). Foreground(cs.Flag), Command: lipgloss.NewStyle(). Foreground(cs.Command), QuotedString: lipgloss.NewStyle(). - PaddingLeft(1). Foreground(cs.QuotedString), }, Span: lipgloss.NewStyle(). diff --git a/vendor/modules.txt b/vendor/modules.txt index 8cbc2b93ffb7ce6c044bca3f157defbf2db3d00c..ebdc8318f987500b38cb989a7a0de6bea45caf5f 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -260,8 +260,8 @@ github.com/charmbracelet/bubbletea/v2 # github.com/charmbracelet/colorprofile v0.3.1 ## explicit; go 1.23.0 github.com/charmbracelet/colorprofile -# github.com/charmbracelet/fang v0.1.0 -## explicit; go 1.23.0 +# github.com/charmbracelet/fang v0.3.1-0.20250711140230-d5ebb8c1d674 +## explicit; go 1.24.0 github.com/charmbracelet/fang # github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe ## explicit; go 1.23.0 @@ -269,7 +269,7 @@ github.com/charmbracelet/glamour/v2 github.com/charmbracelet/glamour/v2/ansi github.com/charmbracelet/glamour/v2/internal/autolink github.com/charmbracelet/glamour/v2/styles -# github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.2.0.20250703152125-8e1c474f8a71 => github.com/charmbracelet/lipgloss-internal/v2 v2.0.0-20250710185058-03664cb9cecb +# github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3 => github.com/charmbracelet/lipgloss-internal/v2 v2.0.0-20250710185058-03664cb9cecb ## explicit; go 1.24.2 github.com/charmbracelet/lipgloss/v2 github.com/charmbracelet/lipgloss/v2/table