Skip to content

Commit dc5978f

Browse files
authored
themes commands (#70)
1 parent f4cb3ac commit dc5978f

5 files changed

Lines changed: 286 additions & 3 deletions

File tree

cmd/themes.go

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
7+
"github.com/loops-so/cli/internal/config"
8+
"github.com/loops-so/loops-go"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
func runThemesGet(cfg *config.Config, id string) (*loops.Theme, error) {
13+
return newAPIClient(cfg).GetTheme(id)
14+
}
15+
16+
func runThemesList(cfg *config.Config, params loops.PaginationParams) ([]loops.Theme, error) {
17+
client := newAPIClient(cfg)
18+
if params.Cursor != "" {
19+
themes, _, err := client.ListThemes(params)
20+
return themes, err
21+
}
22+
return loops.Paginate(func(cursor string) ([]loops.Theme, *loops.Pagination, error) {
23+
return client.ListThemes(loops.PaginationParams{
24+
PerPage: params.PerPage,
25+
Cursor: cursor,
26+
})
27+
})
28+
}
29+
30+
var themesCmd = &cobra.Command{
31+
Use: "themes",
32+
Short: "Manage themes",
33+
Hidden: true,
34+
}
35+
36+
var themesListCmd = &cobra.Command{
37+
Use: "list",
38+
Short: "List themes",
39+
RunE: func(cmd *cobra.Command, args []string) error {
40+
if err := validatePickFlags(cmd); err != nil {
41+
return err
42+
}
43+
44+
cfg, err := loadConfig()
45+
if err != nil {
46+
return err
47+
}
48+
49+
themes, err := runThemesList(cfg, paginationParams(cmd))
50+
if err != nil {
51+
return err
52+
}
53+
54+
if isJSONOutput() {
55+
if themes == nil {
56+
themes = []loops.Theme{}
57+
}
58+
return printJSON(cmd.OutOrStdout(), themes)
59+
}
60+
61+
if len(themes) == 0 {
62+
fmt.Fprintln(cmd.OutOrStdout(), "No themes found.")
63+
return nil
64+
}
65+
66+
headers := []string{"ID", "NAME", "DEFAULT", "UPDATED"}
67+
rows := make([][]string, 0, len(themes))
68+
for _, th := range themes {
69+
rows = append(rows, []string{
70+
th.ThemeID,
71+
th.Name,
72+
strconv.FormatBool(th.IsDefault),
73+
th.UpdatedAt,
74+
})
75+
}
76+
77+
if isPicking(cmd) {
78+
return runPicker(headers, rows, []pickBinding{
79+
copyColumnBinding("enter", "copy id", "theme ID", rows, 0, cmd.OutOrStdout()),
80+
})
81+
}
82+
83+
t := newStyledTable(cmd.OutOrStdout(), headers...)
84+
for _, r := range rows {
85+
t.Row(r...)
86+
}
87+
return t.Render()
88+
},
89+
}
90+
91+
var themesGetCmd = &cobra.Command{
92+
Use: "get <id>",
93+
Short: "Get a theme",
94+
Args: cobra.ExactArgs(1),
95+
RunE: func(cmd *cobra.Command, args []string) error {
96+
cfg, err := loadConfig()
97+
if err != nil {
98+
return err
99+
}
100+
101+
th, err := runThemesGet(cfg, args[0])
102+
if err != nil {
103+
return err
104+
}
105+
106+
if isJSONOutput() {
107+
return printJSON(cmd.OutOrStdout(), th)
108+
}
109+
110+
t := newStyledTable(cmd.OutOrStdout(), "FIELD", "VALUE")
111+
t.Row("themeId", th.ThemeID)
112+
t.Row("name", th.Name)
113+
t.Row("isDefault", strconv.FormatBool(th.IsDefault))
114+
t.Row("createdAt", th.CreatedAt)
115+
t.Row("updatedAt", th.UpdatedAt)
116+
if err := t.Render(); err != nil {
117+
return err
118+
}
119+
120+
fmt.Fprintln(cmd.OutOrStdout())
121+
return printThemeStyles(cmd, th.Styles)
122+
},
123+
}
124+
125+
func printThemeStyles(cmd *cobra.Command, s loops.ThemeStyles) error {
126+
t := newStyledTable(cmd.OutOrStdout(), "STYLE", "VALUE")
127+
for _, row := range themeStyleRows(s) {
128+
t.Row(row[0], dashIfEmpty(row[1]))
129+
}
130+
return t.Render()
131+
}
132+
133+
func dashIfEmpty(s string) string {
134+
if s == "" {
135+
return "-"
136+
}
137+
return s
138+
}
139+
140+
func themeStyleRows(s loops.ThemeStyles) [][2]string {
141+
return [][2]string{
142+
{"backgroundColor", s.BackgroundColor},
143+
{"backgroundXPadding", formatFloat(s.BackgroundXPadding)},
144+
{"backgroundYPadding", formatFloat(s.BackgroundYPadding)},
145+
{"bodyColor", s.BodyColor},
146+
{"bodyXPadding", formatFloat(s.BodyXPadding)},
147+
{"bodyYPadding", formatFloat(s.BodyYPadding)},
148+
{"bodyFontFamily", s.BodyFontFamily},
149+
{"bodyFontCategory", s.BodyFontCategory},
150+
{"borderColor", s.BorderColor},
151+
{"borderWidth", formatFloat(s.BorderWidth)},
152+
{"borderRadius", formatFloat(s.BorderRadius)},
153+
{"buttonBodyColor", s.ButtonBodyColor},
154+
{"buttonBodyXPadding", formatFloat(s.ButtonBodyXPadding)},
155+
{"buttonBodyYPadding", formatFloat(s.ButtonBodyYPadding)},
156+
{"buttonBorderColor", s.ButtonBorderColor},
157+
{"buttonBorderWidth", formatFloat(s.ButtonBorderWidth)},
158+
{"buttonBorderRadius", formatFloat(s.ButtonBorderRadius)},
159+
{"buttonTextColor", s.ButtonTextColor},
160+
{"buttonTextFormat", formatFloat(s.ButtonTextFormat)},
161+
{"buttonTextFontSize", formatFloat(s.ButtonTextFontSize)},
162+
{"dividerColor", s.DividerColor},
163+
{"dividerBorderWidth", formatFloat(s.DividerBorderWidth)},
164+
{"textBaseColor", s.TextBaseColor},
165+
{"textBaseFontSize", formatFloat(s.TextBaseFontSize)},
166+
{"textBaseLineHeight", formatFloat(s.TextBaseLineHeight)},
167+
{"textBaseLetterSpacing", formatFloat(s.TextBaseLetterSpacing)},
168+
{"textLinkColor", s.TextLinkColor},
169+
{"heading1Color", s.Heading1Color},
170+
{"heading1FontSize", formatFloat(s.Heading1FontSize)},
171+
{"heading1LineHeight", formatFloat(s.Heading1LineHeight)},
172+
{"heading1LetterSpacing", formatFloat(s.Heading1LetterSpacing)},
173+
{"heading2Color", s.Heading2Color},
174+
{"heading2FontSize", formatFloat(s.Heading2FontSize)},
175+
{"heading2LineHeight", formatFloat(s.Heading2LineHeight)},
176+
{"heading2LetterSpacing", formatFloat(s.Heading2LetterSpacing)},
177+
{"heading3Color", s.Heading3Color},
178+
{"heading3FontSize", formatFloat(s.Heading3FontSize)},
179+
{"heading3LineHeight", formatFloat(s.Heading3LineHeight)},
180+
{"heading3LetterSpacing", formatFloat(s.Heading3LetterSpacing)},
181+
}
182+
}
183+
184+
func formatFloat(f float64) string {
185+
return strconv.FormatFloat(f, 'f', -1, 64)
186+
}
187+
188+
func init() {
189+
addPaginationFlags(themesListCmd)
190+
addPickFlag(themesListCmd)
191+
themesCmd.AddCommand(themesListCmd)
192+
themesCmd.AddCommand(themesGetCmd)
193+
rootCmd.AddCommand(themesCmd)
194+
}

cmd/themes_get_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package cmd
2+
3+
import (
4+
"net/http"
5+
"testing"
6+
)
7+
8+
func TestRunThemesGet(t *testing.T) {
9+
body := `{
10+
"success": true,
11+
"themeId": "thm_abc123",
12+
"name": "Default",
13+
"isDefault": true,
14+
"createdAt": "2026-04-01T10:00:00Z",
15+
"updatedAt": "2026-04-02T10:00:00Z",
16+
"styles": {
17+
"backgroundColor": "#ffffff",
18+
"bodyColor": "#000000",
19+
"textBaseFontSize": 16,
20+
"heading1FontSize": 32
21+
}
22+
}`
23+
24+
t.Run("returns the theme", func(t *testing.T) {
25+
serveJSON(t, http.StatusOK, body)
26+
th, err := runThemesGet(cfg(t), "thm_abc123")
27+
if err != nil {
28+
t.Fatalf("unexpected error: %v", err)
29+
}
30+
if th.ThemeID != "thm_abc123" {
31+
t.Errorf("ThemeID = %q, want thm_abc123", th.ThemeID)
32+
}
33+
if th.Name != "Default" {
34+
t.Errorf("Name = %q, want Default", th.Name)
35+
}
36+
if !th.IsDefault {
37+
t.Error("IsDefault = false, want true")
38+
}
39+
if th.Styles.BackgroundColor != "#ffffff" {
40+
t.Errorf("Styles.BackgroundColor = %q, want #ffffff", th.Styles.BackgroundColor)
41+
}
42+
if th.Styles.TextBaseFontSize != 16 {
43+
t.Errorf("Styles.TextBaseFontSize = %v, want 16", th.Styles.TextBaseFontSize)
44+
}
45+
})
46+
47+
t.Run("returns error on non-200 response", func(t *testing.T) {
48+
serveJSON(t, http.StatusNotFound, `{"success":false,"message":"Theme not found"}`)
49+
_, err := runThemesGet(cfg(t), "thm_missing")
50+
if err == nil {
51+
t.Fatal("expected error, got nil")
52+
}
53+
})
54+
}

cmd/themes_list_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package cmd
2+
3+
import (
4+
"net/http"
5+
"testing"
6+
7+
"github.com/loops-so/loops-go"
8+
)
9+
10+
func TestRunThemesList(t *testing.T) {
11+
t.Run("returns themes", func(t *testing.T) {
12+
serveJSON(t, http.StatusOK, `{"pagination":{"nextCursor":""},"data":[{"themeId":"thm_1","name":"Default","isDefault":true,"createdAt":"2026-04-01","updatedAt":"2026-04-02","styles":{}}]}`)
13+
themes, err := runThemesList(cfg(t), loops.PaginationParams{})
14+
if err != nil {
15+
t.Fatalf("unexpected error: %v", err)
16+
}
17+
if len(themes) != 1 {
18+
t.Fatalf("expected 1 theme, got %d", len(themes))
19+
}
20+
if themes[0].ThemeID != "thm_1" {
21+
t.Errorf("ThemeID = %q, want thm_1", themes[0].ThemeID)
22+
}
23+
if !themes[0].IsDefault {
24+
t.Error("IsDefault = false, want true")
25+
}
26+
})
27+
28+
t.Run("returns error on api failure", func(t *testing.T) {
29+
serveJSON(t, http.StatusUnauthorized, `{"error":"unauthorized"}`)
30+
_, err := runThemesList(cfg(t), loops.PaginationParams{})
31+
if err == nil {
32+
t.Fatal("expected error, got nil")
33+
}
34+
})
35+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ require (
99
github.com/atotto/clipboard v0.1.4
1010
github.com/charmbracelet/colorprofile v0.4.2
1111
github.com/charmbracelet/x/term v0.2.2
12-
github.com/loops-so/loops-go v0.1.2
12+
github.com/loops-so/loops-go v0.1.3
1313
github.com/spf13/cobra v1.10.2
1414
github.com/zalando/go-keyring v0.2.6
1515
golang.org/x/term v0.41.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,8 +299,8 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
299299
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
300300
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
301301
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
302-
github.com/loops-so/loops-go v0.1.2 h1:ROaDb9LEQvRlZupa1w+OqBDfNKo6agIZhoDYv7hjkes=
303-
github.com/loops-so/loops-go v0.1.2/go.mod h1:BDzBhAn/4e2QSKXrpXufIpSuH8xUPv9oa+hazH01ejE=
302+
github.com/loops-so/loops-go v0.1.3 h1:JZqbHBE6T3e6UoJNVydP/I6Ie4mW94IW5uVbwTC+rXM=
303+
github.com/loops-so/loops-go v0.1.3/go.mod h1:BDzBhAn/4e2QSKXrpXufIpSuH8xUPv9oa+hazH01ejE=
304304
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
305305
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
306306
github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=

0 commit comments

Comments
 (0)