Skip to content

Commit 40e61f0

Browse files
committed
feat: improve rendering
1 parent 18f44e6 commit 40e61f0

6 files changed

Lines changed: 271 additions & 139 deletions

File tree

cmd/root.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os/signal"
77
"syscall"
88

9+
"github.com/jgfranco17/delphi-cli/internal/errorhandling"
910
"github.com/jgfranco17/delphi-cli/internal/logging"
1011
"github.com/sirupsen/logrus"
1112
"github.com/spf13/cobra"
@@ -59,6 +60,8 @@ func Execute() {
5960
ctx := context.Background()
6061
cli := NewCLI()
6162
if err := cli.ExecuteContext(ctx); err != nil {
63+
errorHandler := errorhandling.New(os.Stderr)
64+
errorHandler.RenderError(err)
6265
os.Exit(1)
6366
}
6467
}

cmd/statusline.go

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,62 @@
11
package cmd
22

33
import (
4+
"fmt"
45
"os"
56

67
"github.com/fatih/color"
78
"github.com/jgfranco17/delphi-cli/internal/tooling/render"
89
"github.com/spf13/cobra"
910
)
1011

12+
const (
13+
noColorFlagName = "disable-colors"
14+
showAllFlagName = "all"
15+
showCwdFlagName = "cwd"
16+
showGitFlagName = "git"
17+
showUsageFlagName = "stats"
18+
)
19+
20+
func init() {
21+
color.NoColor = false
22+
}
23+
1124
func newStatuslineCmd() *cobra.Command {
12-
const forceColorFlagName = "force-color"
13-
var forceColor bool
25+
var noColor bool
26+
var showAll bool
27+
var showCwd bool
28+
var showGit bool
29+
var showUsage bool
1430

1531
cmd := &cobra.Command{
1632
Use: "statusline",
17-
Short: "Render Claude Code statusline from JSON piped on stdin",
33+
Short: "Render Claude Code statusline from JSON piped in stdin",
34+
Long: `Render Claude Code statusline from JSON piped in stdin.
35+
36+
The statusline command reads a JSON blob from stdin representing the agent's state and renders a formatted statusline to stdout.
37+
This is intended for use by shell integrations to display the current model, workspace, git status, and usage stats.`,
1838
RunE: func(cmd *cobra.Command, args []string) error {
19-
forceColor, _ := cmd.Flags().GetBool(forceColorFlagName)
20-
if forceColor {
21-
color.NoColor = false
39+
if noColor {
40+
color.NoColor = true
41+
}
42+
opts := render.Options{
43+
OutputWriter: cmd.OutOrStdout(),
44+
WithCurrentDir: showAll || showCwd,
45+
WithGitInfo: showAll || showGit,
46+
WithUsageStats: showAll || showUsage,
47+
}
48+
status := render.New(opts)
49+
if err := status.GenerateFrom(cmd.Context(), os.Stdin); err != nil {
50+
return fmt.Errorf("failed to render status line: %w", err)
2251
}
23-
renderer := render.New(os.Stdout)
24-
return renderer.GenerateFrom(cmd.Context(), os.Stdin)
52+
return nil
2553
},
2654
}
2755

28-
cmd.Flags().BoolVar(&forceColor, forceColorFlagName, false, "Force ANSI color output formatting even when stdout is not a TTY")
56+
cmd.Flags().BoolVar(&noColor, noColorFlagName, false, "Disable ANSI color output formatting")
57+
cmd.Flags().BoolVar(&showAll, showAllFlagName, false, "Show all statusline sections")
58+
cmd.Flags().BoolVar(&showCwd, showCwdFlagName, false, "Show current working directory")
59+
cmd.Flags().BoolVar(&showGit, showGitFlagName, false, "Show git information")
60+
cmd.Flags().BoolVar(&showUsage, showUsageFlagName, false, "Show usage statistics")
2961
return cmd
3062
}

go.sum

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
21
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
32
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
43
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -12,7 +11,6 @@ github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw
1211
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
1312
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
1413
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
15-
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
1614
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
1715
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
1816
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
@@ -21,11 +19,8 @@ github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiT
2119
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
2220
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
2321
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
24-
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
25-
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
2622
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
2723
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
28-
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
2924
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
3025
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
3126
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package errorhandling
2+
3+
import (
4+
"io"
5+
6+
"github.com/fatih/color"
7+
)
8+
9+
type Handler struct {
10+
writer io.Writer
11+
}
12+
13+
func New(w io.Writer) *Handler {
14+
return &Handler{writer: w}
15+
}
16+
17+
func (h *Handler) RenderError(err error) {
18+
redFmt := color.New(color.FgRed, color.Bold)
19+
redFmt.Fprintf(h.writer, "[ERROR]: %v\n", err)
20+
}

internal/tooling/render/renderer.go

Lines changed: 65 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,41 @@ import (
55
"encoding/json"
66
"fmt"
77
"io"
8+
"os"
89

910
"github.com/jgfranco17/delphi-cli/internal/tooling/git"
1011
"github.com/jgfranco17/delphi-cli/internal/tooling/model"
1112
)
1213

13-
// Renderer writes a formatted statusline to an io.Writer.
14-
type Renderer struct {
15-
out io.Writer
16-
git git.Provider
14+
// StatusLine writes a formatted statusline to a writer.
15+
type StatusLine struct {
16+
out io.Writer
17+
git git.Provider
18+
options Options
1719
}
1820

19-
func NewRenderer(out io.Writer, g git.Provider) *Renderer {
20-
return &Renderer{out: out, git: g}
21+
type Options struct {
22+
OutputWriter io.Writer
23+
WithCurrentDir bool
24+
WithGitInfo bool
25+
WithUsageStats bool
2126
}
2227

23-
// New creates a Renderer with the real git provider writing to out.
24-
func New(out io.Writer) *Renderer {
25-
return NewRenderer(out, git.NewExecProvider())
28+
func newRenderer(out io.Writer, g git.Provider, opts Options) *StatusLine {
29+
return &StatusLine{out: out, git: g, options: opts}
30+
}
31+
32+
// New creates a StatusLine with the real git provider writing to out.
33+
func New(opts Options) *StatusLine {
34+
out := opts.OutputWriter
35+
if out == nil {
36+
out = os.Stdout
37+
}
38+
return newRenderer(out, git.NewExecProvider(), opts)
2639
}
2740

2841
// GenerateFrom decodes JSON from r and renders the statusline.
29-
func (r *Renderer) GenerateFrom(ctx context.Context, reader io.Reader) error {
42+
func (r *StatusLine) GenerateFrom(ctx context.Context, reader io.Reader) error {
3043
data, err := io.ReadAll(reader)
3144
if err != nil {
3245
return fmt.Errorf("failed reading input: %w", err)
@@ -38,40 +51,51 @@ func (r *Renderer) GenerateFrom(ctx context.Context, reader io.Reader) error {
3851
return r.Render(ctx, &input)
3952
}
4053

41-
// Render writes the three-line statusline for the given input.
42-
func (r *Renderer) Render(ctx context.Context, input *model.AgentInput) error {
43-
pct := int(input.ContextWindow.UsedPercentage)
44-
bar := input.ContextWindow.Render()
45-
costFmt := input.Cost.Format()
46-
limits := input.RateLimits.Format()
47-
gitInfo := r.formatGitInfo(ctx, input.Workspace.CurrentDir)
48-
49-
fmt.Fprintf(
50-
r.out, "%s %s %s %s\n",
51-
model.ColorDim.Sprint("Using"),
52-
model.ColorBoldCyan.Sprint(input.Model.DisplayName),
53-
model.ColorDim.Sprint("in"),
54-
model.ColorYellow.Sprint(input.Workspace.CurrentDir),
55-
)
56-
fmt.Fprintf(
57-
r.out, "%s %s %s %s %s %s %s\n",
58-
model.ColorDim.Sprint("Usage:"),
59-
input.ContextWindow.Color().Sprint(bar),
60-
model.ColorBold.Sprintf("%d%%", pct),
61-
model.ColorDim.Sprint("|"),
62-
model.ColorGreen.Sprintf("~%s equiv", costFmt),
63-
model.ColorDim.Sprint("|"),
64-
model.ColorMagenta.Sprint(limits),
65-
)
66-
fmt.Fprintf(
67-
r.out, "%s %s\n",
68-
model.ColorDim.Sprint("Git:"),
69-
gitInfo,
70-
)
54+
// Render writes the statusline for the given input, gated by Options.
55+
func (r *StatusLine) Render(ctx context.Context, input *model.AgentInput) error {
56+
if r.options.WithCurrentDir {
57+
fmt.Fprintf(
58+
r.out, "%s %s %s %s\n",
59+
model.ColorDim.Sprint("Using"),
60+
model.ColorBoldCyan.Sprint(input.Model.DisplayName),
61+
model.ColorDim.Sprint("in"),
62+
model.ColorYellow.Sprint(input.Workspace.CurrentDir),
63+
)
64+
} else {
65+
fmt.Fprintf(
66+
r.out, "%s %s\n",
67+
model.ColorDim.Sprint("Using"),
68+
model.ColorBoldCyan.Sprint(input.Model.DisplayName),
69+
)
70+
}
71+
if r.options.WithUsageStats {
72+
pct := int(input.ContextWindow.UsedPercentage)
73+
bar := input.ContextWindow.Render()
74+
costFmt := input.Cost.Format()
75+
limits := input.RateLimits.Format()
76+
fmt.Fprintf(
77+
r.out, "%s %s %s %s %s %s %s\n",
78+
model.ColorDim.Sprint("Usage:"),
79+
input.ContextWindow.Color().Sprint(bar),
80+
model.ColorBold.Sprintf("%d%%", pct),
81+
model.ColorDim.Sprint("|"),
82+
model.ColorGreen.Sprintf("~%s equiv", costFmt),
83+
model.ColorDim.Sprint("|"),
84+
model.ColorMagenta.Sprint(limits),
85+
)
86+
}
87+
if r.options.WithGitInfo {
88+
gitInfo := r.formatGitInfo(ctx, input.Workspace.CurrentDir)
89+
fmt.Fprintf(
90+
r.out, "%s %s\n",
91+
model.ColorDim.Sprint("Git:"),
92+
gitInfo,
93+
)
94+
}
7195
return nil
7296
}
7397

74-
func (r *Renderer) formatGitInfo(ctx context.Context, dir string) string {
98+
func (r *StatusLine) formatGitInfo(ctx context.Context, dir string) string {
7599
branch, dirty, err := r.git.BranchStatus(ctx, dir)
76100
if err != nil || branch == "" {
77101
return "none"

0 commit comments

Comments
 (0)