Skip to content

Commit 9eda692

Browse files
authored
Feat/tui command palette (#30)
* refactor(tui): simplify the home command palette * refactor(tui): group home actions by category * refactor(tui): polish category action ordering * refactor(tui): flatten support actions into root menu * refactor(tui): simplify shared terminal screens * refactor(tui): streamline workflow report output * refactor(tui): tighten workflow wording * refactor(tui): simplify the project summary block * fix(tui): run leaf actions directly from home * feat(tui): confirm support actions before opening * chore: release v1.7.0
1 parent 797306c commit 9eda692

14 files changed

Lines changed: 413 additions & 165 deletions

cmd/wfkit/docs_flow.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,9 @@ func (f *docsFlow) loadConfig() error {
8383

8484
func (f *docsFlow) printHeader() {
8585
utils.PrintSection("Docs Hub")
86-
utils.PrintKeyValue("Webflow", f.baseURL)
87-
utils.PrintKeyValue("Markdown", f.options.EntryPath)
88-
utils.PrintKeyValue("Page slug", f.options.PageSlug)
86+
utils.PrintKeyValue("Site", f.baseURL)
87+
utils.PrintKeyValue("Entry", f.options.EntryPath)
88+
utils.PrintKeyValue("Page", f.options.PageSlug)
8989
fmt.Println()
9090
}
9191

cmd/wfkit/docs_report.go

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ import (
1010
func printDocsTimeline(authed, planned, applied bool) {
1111
utils.PrintTimeline(
1212
"Docs Timeline",
13-
utils.TimelineStep{Label: "Authenticate", Status: timelineStatus(authed, false), Details: timelineDetails(authed, "Webflow session ready")},
14-
utils.TimelineStep{Label: "Plan docs hub", Status: timelineStatus(planned, false), Details: timelineDetails(planned, "markdown rendered and target page prepared")},
15-
utils.TimelineStep{Label: "Apply docs hub", Status: timelineStatus(applied, false), Details: timelineDetails(applied, "page created or docs block updated")},
13+
utils.TimelineStep{Label: "Auth", Status: timelineStatus(authed, false), Details: timelineDetails(authed, "session ready")},
14+
utils.TimelineStep{Label: "Plan", Status: timelineStatus(planned, false), Details: timelineDetails(planned, "page prepared")},
15+
utils.TimelineStep{Label: "Apply", Status: timelineStatus(applied, false), Details: timelineDetails(applied, "page updated")},
1616
)
1717
}
1818

1919
func printDocsPlan(plan publish.DocsHubPlan) {
20-
utils.PrintSection("Docs Hub Plan")
20+
utils.PrintSection("Docs Hub")
2121
utils.PrintStatus(docsStatus(plan.Action), plan.PageSlug, plan.Message)
2222
utils.PrintKeyValue("Markdown", plan.EntryPath)
2323
utils.PrintKeyValue("Target page", displayValue(plan.PageTitle))
@@ -28,11 +28,9 @@ func printDocsPlan(plan publish.DocsHubPlan) {
2828

2929
func printDocsResult(result publish.DocsHubResult) {
3030
utils.PrintSection("Docs Result")
31-
utils.PrintSummary(
32-
utils.SummaryMetric{Label: "Created", Value: map[bool]string{true: "yes", false: "no"}[result.Created], Tone: "info"},
33-
utils.SummaryMetric{Label: "Updated", Value: map[bool]string{true: "yes", false: "no"}[result.Updated], Tone: "success"},
34-
utils.SummaryMetric{Label: "Published", Value: map[bool]string{true: "yes", false: "no"}[result.Published], Tone: "info"},
35-
)
31+
utils.PrintStatus("INFO", "Created", map[bool]string{true: "yes", false: "no"}[result.Created])
32+
utils.PrintStatus("OK", "Updated", map[bool]string{true: "yes", false: "no"}[result.Updated])
33+
utils.PrintStatus("INFO", "Published", map[bool]string{true: "yes", false: "no"}[result.Published])
3634
fmt.Println()
3735
}
3836

cmd/wfkit/doctor.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ func printDoctorReport(checks []doctorCheck) {
6060
utils.PrintStatus(string(check.Status), check.Name, check.Message)
6161
}
6262

63+
utils.PrintSection("Summary")
64+
utils.PrintStatus(overallDoctorStatus(warnCount, failCount), "Overall", overallDoctorMessage(warnCount, failCount))
6365
utils.PrintSummary(
6466
utils.SummaryMetric{Label: "Passed", Value: fmt.Sprintf("%d", passCount), Tone: "success"},
6567
utils.SummaryMetric{Label: "Warnings", Value: fmt.Sprintf("%d", warnCount), Tone: "warning"},
@@ -136,6 +138,28 @@ func doctorDashboardCards(checks []doctorCheck) []utils.DashboardCard {
136138
return cards
137139
}
138140

141+
func overallDoctorStatus(warnCount, failCount int) string {
142+
switch {
143+
case failCount > 0:
144+
return "FAIL"
145+
case warnCount > 0:
146+
return "WARN"
147+
default:
148+
return "OK"
149+
}
150+
}
151+
152+
func overallDoctorMessage(warnCount, failCount int) string {
153+
switch {
154+
case failCount > 0:
155+
return "Resolve blocking issues before relying on this setup."
156+
case warnCount > 0:
157+
return "Core setup works, but a few things are still worth checking."
158+
default:
159+
return "Everything looks ready."
160+
}
161+
}
162+
139163
func checkFileExists(name, path string) doctorCheck {
140164
if _, err := os.Stat(path); err != nil {
141165
if os.IsNotExist(err) {

cmd/wfkit/interactive_command_flows_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"flag"
5+
"strings"
56
"testing"
67

78
"github.com/urfave/cli/v2"
@@ -113,3 +114,29 @@ func TestInteractiveDoctorFlowBuildsContext(t *testing.T) {
113114
t.Fatal("expected skip-auth to be set")
114115
}
115116
}
117+
118+
func TestInteractiveSupportFlowMetadata(t *testing.T) {
119+
parent := cli.NewContext(&cli.App{Version: "1.6.0"}, flag.NewFlagSet("wfkit", flag.ContinueOnError), nil)
120+
121+
title, description, target := newInteractiveSupportFlow(parent, "request_feature").metadata()
122+
if title != "Request a feature" {
123+
t.Fatalf("unexpected feature title: %q", title)
124+
}
125+
if !strings.Contains(description, "feature request form") {
126+
t.Fatalf("unexpected feature description: %q", description)
127+
}
128+
if !strings.Contains(target, "template=feature_request.yml") {
129+
t.Fatalf("unexpected feature target: %q", target)
130+
}
131+
132+
title, description, target = newInteractiveSupportFlow(parent, "report_bug").metadata()
133+
if title != "Report a bug" {
134+
t.Fatalf("unexpected bug title: %q", title)
135+
}
136+
if !strings.Contains(description, "bug report form") {
137+
t.Fatalf("unexpected bug description: %q", description)
138+
}
139+
if !strings.Contains(target, "template=bug_report.yml") {
140+
t.Fatalf("unexpected bug target: %q", target)
141+
}
142+
}

cmd/wfkit/interactive_flow.go

Lines changed: 152 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package main
22

33
import (
44
"fmt"
5+
"strings"
56

7+
"wfkit/internal/config"
68
"wfkit/internal/updater"
79
"wfkit/internal/utils"
810

@@ -12,6 +14,7 @@ import (
1214

1315
type interactiveFlow struct {
1416
cliContext *cli.Context
17+
category string
1518
action string
1619
}
1720

@@ -23,45 +26,97 @@ func (f *interactiveFlow) run() error {
2326
for {
2427
f.printHeader()
2528

26-
if err := f.selectAction(); err != nil {
29+
if err := f.selectCategory(); err != nil {
2730
return err
2831
}
2932

30-
if f.action == "exit" {
33+
if f.category == "exit" {
3134
utils.CPrint("Goodbye!", "cyan")
3235
return nil
3336
}
3437

35-
utils.ClearScreen()
36-
if err := f.dispatch(); err != nil {
37-
return err
38+
if action, ok := categoryAction(f.category); ok {
39+
f.action = action
40+
utils.ClearScreen()
41+
if err := f.dispatch(); err != nil {
42+
return err
43+
}
44+
continue
45+
}
46+
47+
for {
48+
utils.ClearScreen()
49+
f.printHeader()
50+
if err := f.selectAction(); err != nil {
51+
return err
52+
}
53+
if f.action == "back" {
54+
break
55+
}
56+
utils.ClearScreen()
57+
if err := f.dispatch(); err != nil {
58+
return err
59+
}
3860
}
3961
}
4062
}
4163

4264
func (f *interactiveFlow) printHeader() {
4365
version := f.cliContext.App.Version
4466
utils.PrintAppHeader(version, "Build Webflow scripts locally, proxy safely, and publish with confidence.")
45-
46-
utils.PrintSection("Quick Start")
47-
for _, item := range interactiveQuickStartItems() {
48-
utils.PrintStatus("READY", item.title, item.description)
49-
}
50-
fmt.Println()
67+
f.printProjectSummary()
5168

5269
if updateManager := updater.NewUpdateManager(version); updateManager != nil {
5370
if result, err := updateManager.Check(updater.CheckOptions{AllowStale: true}); err == nil && result.Available {
54-
utils.PrintUpdateBanner(version, result.LatestVersion)
71+
f.printUpdateNotice(version, result.LatestVersion)
5572
}
5673
}
5774
}
5875

76+
func (f *interactiveFlow) printProjectSummary() {
77+
cfg, err := config.ReadConfig()
78+
if err != nil {
79+
utils.PrintSection("Project")
80+
utils.PrintStatus("WARN", "Config", err.Error())
81+
fmt.Println()
82+
return
83+
}
84+
85+
utils.PrintSection("Project")
86+
utils.PrintKeyValue("App", displayValue(cfg.AppName))
87+
utils.PrintKeyValue("Site", displayValue(cfg.EffectiveSiteURL()))
88+
utils.PrintKeyValue("Package", displayValue(cfg.PackageManager))
89+
utils.PrintKeyValue("Delivery", displayValue(cfg.DeliveryMode))
90+
utils.PrintKeyValue("Assets", displayValue(cfg.AssetBranch))
91+
utils.PrintKeyValue("Docs", displayValue(cfg.DocsPageSlug))
92+
utils.PrintKeyValue("Build", displayValue(cfg.BuildDir))
93+
fmt.Println()
94+
}
95+
96+
func (f *interactiveFlow) printUpdateNotice(currentVersion, latestVersion string) {
97+
utils.PrintStatus("WARN", fmt.Sprintf("Update available: v%s", latestVersion), compactUpdateMessage(currentVersion, latestVersion))
98+
fmt.Println()
99+
}
100+
101+
func (f *interactiveFlow) selectCategory() error {
102+
return huh.NewForm(
103+
huh.NewGroup(
104+
huh.NewSelect[string]().
105+
Title("Categories").
106+
Description("Choose a category first. Esc or Ctrl+C exits.").
107+
Options(interactiveCategoryOptions()...).
108+
Value(&f.category),
109+
),
110+
).Run()
111+
}
112+
59113
func (f *interactiveFlow) selectAction() error {
60114
return huh.NewForm(
61115
huh.NewGroup(
62116
huh.NewSelect[string]().
63-
Title("What would you like to do?").
64-
Options(interactiveActionOptions()...).
117+
Title(categoryTitle(f.category)).
118+
Description("Type to filter. Enter selects. Esc returns to the previous screen.").
119+
Options(interactiveActionOptions(f.category)...).
65120
Value(&f.action),
66121
),
67122
).Run()
@@ -90,50 +145,99 @@ func (f *interactiveFlow) dispatch() error {
90145
case "update":
91146
return updateMode(f.cliContext)
92147
case "report_bug":
93-
return openBugReport(f.cliContext)
148+
return newInteractiveSupportFlow(f.cliContext, "report_bug").run()
94149
case "request_feature":
95-
return openFeatureRequest(f.cliContext)
150+
return newInteractiveSupportFlow(f.cliContext, "request_feature").run()
96151
default:
97152
return nil
98153
}
99154
}
100155

101-
type interactiveQuickStartItem struct {
102-
title string
103-
description string
156+
func interactiveCategoryOptions() []huh.Option[string] {
157+
return []huh.Option[string]{
158+
huh.NewOption("Develop", "develop"),
159+
huh.NewOption("Ship", "ship"),
160+
huh.NewOption("Content", "content"),
161+
huh.NewOption("Project", "project"),
162+
huh.NewOption("Check for updates", "update"),
163+
huh.NewOption("Request a feature", "request_feature"),
164+
huh.NewOption("Report a bug", "report_bug"),
165+
huh.NewOption("Exit", "exit"),
166+
}
167+
}
168+
169+
func interactiveActionOptions(category string) []huh.Option[string] {
170+
switch category {
171+
case "develop":
172+
return []huh.Option[string]{
173+
huh.NewOption("Proxy local site", "proxy_dev"),
174+
huh.NewOption("Migrate code", "migrate"),
175+
huh.NewOption("Run doctor", "doctor"),
176+
huh.NewOption("Back", "back"),
177+
}
178+
case "ship":
179+
return []huh.Option[string]{
180+
huh.NewOption("Publish code", "publish"),
181+
huh.NewOption("Publish docs", "docs"),
182+
huh.NewOption("Back", "back"),
183+
}
184+
case "content":
185+
return []huh.Option[string]{
186+
huh.NewOption("Manage pages", "pages"),
187+
huh.NewOption("Manage CMS", "cms"),
188+
huh.NewOption("Back", "back"),
189+
}
190+
case "project":
191+
return []huh.Option[string]{
192+
huh.NewOption("Initialize project", "init"),
193+
huh.NewOption("Configure defaults", "config"),
194+
huh.NewOption("Back", "back"),
195+
}
196+
default:
197+
return []huh.Option[string]{huh.NewOption("Back", "back")}
198+
}
104199
}
105200

106-
func interactiveQuickStartItems() []interactiveQuickStartItem {
107-
return []interactiveQuickStartItem{
108-
{
109-
title: "Initialize",
110-
description: "Scaffold a Webflow-ready Vite project with pages, globals, and config.",
111-
},
112-
{
113-
title: "Develop",
114-
description: "Proxy the live site locally and inject your dev entry without touching production.",
115-
},
116-
{
117-
title: "Docs",
118-
description: "Render markdown and publish a dedicated documentation page inside Webflow.",
119-
},
201+
func categoryTitle(category string) string {
202+
switch category {
203+
case "develop":
204+
return "Develop"
205+
case "ship":
206+
return "Ship"
207+
case "content":
208+
return "Content"
209+
case "project":
210+
return "Project"
211+
case "update":
212+
return "Check for updates"
213+
case "request_feature":
214+
return "Request a feature"
215+
case "report_bug":
216+
return "Report a bug"
217+
default:
218+
return "Actions"
120219
}
121220
}
122221

123-
func interactiveActionOptions() []huh.Option[string] {
124-
return []huh.Option[string]{
125-
huh.NewOption("Initialize a project", "init"),
126-
huh.NewOption("Publish docs", "docs"),
127-
huh.NewOption("Manage pages", "pages"),
128-
huh.NewOption("Manage CMS", "cms"),
129-
huh.NewOption("Migrate page code", "migrate"),
130-
huh.NewOption("Publish code to Webflow", "publish"),
131-
huh.NewOption("Start dev proxy", "proxy_dev"),
132-
huh.NewOption("Run doctor", "doctor"),
133-
huh.NewOption("Configure CLI defaults", "config"),
134-
huh.NewOption("Check for updates", "update"),
135-
huh.NewOption("Report a bug", "report_bug"),
136-
huh.NewOption("Request a feature", "request_feature"),
137-
huh.NewOption("Exit", "exit"),
222+
func categoryAction(category string) (string, bool) {
223+
switch category {
224+
case "update", "request_feature", "report_bug":
225+
return category, true
226+
default:
227+
return "", false
228+
}
229+
}
230+
231+
func compactUpdateMessage(currentVersion, latestVersion string) string {
232+
parts := make([]string, 0, 2)
233+
if strings.TrimSpace(currentVersion) != "" {
234+
parts = append(parts, "current v"+currentVersion)
235+
}
236+
if strings.TrimSpace(latestVersion) != "" {
237+
parts = append(parts, "latest v"+latestVersion)
238+
}
239+
if len(parts) == 0 {
240+
return "Run `wfkit update` when ready."
138241
}
242+
return strings.Join(parts, " ") + " Run `wfkit update` when ready."
139243
}

0 commit comments

Comments
 (0)