mirror of
https://github.com/charmbracelet/crush.git
synced 2025-08-02 05:20:46 +03:00
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 <caarlos0@users.noreply.github.com> * fix: ctrl+c in raw mode * fix: sigkill can't be handled --------- Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
8ff604f908
commit
2aa5f6151e
1
.gitignore
vendored
1
.gitignore
vendored
@@ -16,6 +16,7 @@
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# IDE specific files
|
||||
.idea/
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
4
go.mod
4
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
|
||||
|
||||
4
go.sum
4
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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}()
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
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
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case resp := <-ch:
|
||||
if cfg.Options.DebugLSP {
|
||||
slog.Debug("Received response", "id", id)
|
||||
}
|
||||
// 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
|
||||
if resp.Error != nil {
|
||||
return fmt.Errorf("request failed: %s (code: %d)", resp.Error.Message, resp.Error.Code)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Notify sends a notification (a request without an ID that doesn't expect a response)
|
||||
|
||||
60
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/go.work.sum
generated
vendored
60
vendor/github.com/Azure/azure-sdk-for-go/sdk/azidentity/go.work.sum
generated
vendored
@@ -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=
|
||||
7
vendor/github.com/charmbracelet/fang/README.md
generated
vendored
7
vendor/github.com/charmbracelet/fang/README.md
generated
vendored
@@ -1,7 +1,7 @@
|
||||
# Fang
|
||||
|
||||
<p>
|
||||
<img width="485" alt="Charm Fang" src="https://github.com/user-attachments/assets/3f34ea01-3750-4760-beb2-a1b700e110f5">
|
||||
<img width="485" alt="Charm Fang" src="https://github.com/user-attachments/assets/3f34ea01-3750-4760-beb2-a1b700e110f5">
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://github.com/charmbracelet/fang/releases"><img src="https://img.shields.io/github/release/charmbracelet/fang.svg" alt="Latest Release"></a>
|
||||
@@ -12,7 +12,7 @@
|
||||
The CLI starter kit. A small, experimental library for batteries-included [Cobra][cobra] applications.
|
||||
|
||||
<p>
|
||||
<img width="865" alt="fang-02" src="https://github.com/user-attachments/assets/7f68ec3f-2b42-4188-a750-7e2808696132" />
|
||||
<img width="859" alt="The Charm Fang mascot and title treatment" src="https://github.com/user-attachments/assets/5c35e1fa-9577-4f81-a879-3ddb4d4a43f0" />
|
||||
</p>
|
||||
|
||||
## 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)
|
||||
}
|
||||
}
|
||||
|
||||
119
vendor/github.com/charmbracelet/fang/fang.go
generated
vendored
119
vendor/github.com/charmbracelet/fang/fang.go
generated
vendored
@@ -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.signals) > 0 {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = signal.NotifyContext(ctx, opts.signals...)
|
||||
defer cancel()
|
||||
}
|
||||
if len(opts.commit) >= shaLen {
|
||||
opts.version += " (" + opts.commit[:shaLen] + ")"
|
||||
}
|
||||
|
||||
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 ""
|
||||
|
||||
302
vendor/github.com/charmbracelet/fang/help.go
generated
vendored
302
vendor/github.com/charmbracelet/fang/help.go
generated
vendored
@@ -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)
|
||||
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(" ")
|
||||
}
|
||||
|
||||
if isRedirecting {
|
||||
args[i] += styles.Program.DimmedArgument.Render(arg)
|
||||
isRedirecting = false
|
||||
continue
|
||||
}
|
||||
|
||||
quoteStart := arg[0] == '"'
|
||||
quoteEnd := arg[len(arg)-1] == '"'
|
||||
flagStart := arg[0] == '-'
|
||||
if i == 1 && !quoteStart && !flagStart {
|
||||
args[i] = styles.Program.Command.Render(arg)
|
||||
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
|
||||
}
|
||||
return flag.Value.Type() == "bool"
|
||||
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 false
|
||||
}
|
||||
|
||||
52
vendor/github.com/charmbracelet/fang/theme.go
generated
vendored
52
vendor/github.com/charmbracelet/fang/theme.go
generated
vendored
@@ -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().
|
||||
|
||||
6
vendor/modules.txt
vendored
6
vendor/modules.txt
vendored
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user