Skip to content
This repository was archived by the owner on Oct 21, 2024. It is now read-only.

Commit 96aac24

Browse files
committed
tui: mosaic prototype
1 parent 92f90da commit 96aac24

32 files changed

+3391
-728
lines changed

.air.toml

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
root = "."
2+
testdata_dir = "testdata"
3+
tmp_dir = "tmp"
4+
5+
[build]
6+
args_bin = []
7+
bin = "./dist/sst"
8+
cmd = "go build -o ./dist/sst ./cmd/sst"
9+
delay = 1000
10+
exclude_dir = ["assets", "tmp", "vendor", "testdata", "node_modules", "examples"]
11+
exclude_file = []
12+
exclude_regex = ["_test.go"]
13+
exclude_unchanged = false
14+
follow_symlink = false
15+
full_bin = ""
16+
include_dir = []
17+
include_ext = ["go", "tpl", "tmpl", "html"]
18+
include_file = []
19+
kill_delay = "0s"
20+
log = "build-errors.log"
21+
poll = false
22+
poll_interval = 0
23+
post_cmd = []
24+
pre_cmd = []
25+
rerun = false
26+
rerun_delay = 500
27+
send_interrupt = false
28+
stop_on_error = false
29+
30+
[color]
31+
app = ""
32+
build = "yellow"
33+
main = "magenta"
34+
runner = "green"
35+
watcher = "cyan"
36+
37+
[log]
38+
main_only = false
39+
time = false
40+
41+
[misc]
42+
clean_on_exit = false
43+
44+
[proxy]
45+
app_port = 0
46+
enabled = false
47+
proxy_port = 0
48+
49+
[screen]
50+
clear_on_rebuild = false
51+
keep_scroll = true

cmd/sst/cli/cli.go

+339
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
package cli
2+
3+
import (
4+
"context"
5+
"fmt"
6+
flag "github.com/spf13/pflag"
7+
"os"
8+
"os/user"
9+
"strings"
10+
11+
"github.com/charmbracelet/huh"
12+
"github.com/fatih/color"
13+
"github.com/joho/godotenv"
14+
"github.com/sst/ion/internal/util"
15+
"github.com/sst/ion/pkg/project"
16+
)
17+
18+
type Cli struct {
19+
version string
20+
flags map[string]interface{}
21+
arguments []string
22+
path CommandPath
23+
Context context.Context
24+
cancel context.CancelFunc
25+
}
26+
27+
func New(ctx context.Context, cancel context.CancelFunc, root *Command, version string) (*Cli, error) {
28+
parsedFlags := map[string]interface{}{}
29+
root.init(parsedFlags)
30+
flag.CommandLine.Init("sst", flag.ContinueOnError)
31+
cliParseError := flag.CommandLine.Parse(os.Args[1:])
32+
positionals := []string{}
33+
cmds := CommandPath{
34+
*root,
35+
}
36+
for i, arg := range flag.Args() {
37+
var cmd *Command
38+
39+
last := cmds[len(cmds)-1]
40+
if len(last.Children) == 0 {
41+
positionals = flag.Args()[i:]
42+
break
43+
}
44+
for _, c := range last.Children {
45+
if c.Name == arg {
46+
cmd = c
47+
break
48+
}
49+
}
50+
if cmd == nil {
51+
break
52+
}
53+
cmds = append(cmds, *cmd)
54+
}
55+
cli := &Cli{
56+
flags: parsedFlags,
57+
version: version,
58+
arguments: positionals,
59+
path: cmds,
60+
Context: ctx,
61+
cancel: cancel,
62+
}
63+
if cliParseError != nil {
64+
return nil, cli.PrintHelp()
65+
}
66+
cli.configureLog()
67+
return cli, nil
68+
}
69+
70+
func (c *Cli) Run() error {
71+
active := c.path[len(c.path)-1]
72+
required := 0
73+
for _, arg := range active.Args {
74+
if !arg.Required {
75+
continue
76+
}
77+
required += 1
78+
}
79+
if c.Bool("help") || active.Run == nil || len(c.arguments) < required {
80+
return c.PrintHelp()
81+
} else {
82+
return active.Run(c)
83+
}
84+
}
85+
86+
func (c *Cli) Cancel() {
87+
c.cancel()
88+
}
89+
90+
func (c *Cli) Path() CommandPath {
91+
return c.path
92+
}
93+
94+
func (c *Cli) String(name string) string {
95+
if f, ok := c.flags[name]; ok {
96+
return *f.(*string)
97+
}
98+
return ""
99+
}
100+
101+
func (c *Cli) Bool(name string) bool {
102+
if f, ok := c.flags[name]; ok {
103+
return *f.(*bool)
104+
}
105+
return false
106+
}
107+
108+
func (c *Cli) PrintHelp() error {
109+
return c.path.PrintHelp()
110+
}
111+
112+
func (c *Cli) Arguments() []string {
113+
return c.arguments
114+
}
115+
116+
func (c *Cli) Positional(index int) string {
117+
if index >= len(c.arguments) {
118+
return ""
119+
}
120+
return c.arguments[index]
121+
}
122+
123+
type Command struct {
124+
Name string `json:"name"`
125+
Hidden bool `json:"hidden"`
126+
Description Description `json:"description"`
127+
Args ArgumentList `json:"args"`
128+
Flags []Flag `json:"flags"`
129+
Examples []Example `json:"examples"`
130+
Children []*Command `json:"children"`
131+
Run func(cli *Cli) error `json:"-"`
132+
}
133+
134+
func (c *Command) init(parsed map[string]interface{}) {
135+
if c.Args == nil {
136+
c.Args = ArgumentList{}
137+
}
138+
if c.Flags == nil {
139+
c.Flags = []Flag{}
140+
}
141+
if c.Examples == nil {
142+
c.Examples = []Example{}
143+
}
144+
if c.Children == nil {
145+
c.Children = []*Command{}
146+
}
147+
for _, f := range c.Flags {
148+
if parsed[f.Name] != nil {
149+
continue
150+
}
151+
if f.Type == "string" {
152+
parsed[f.Name] = flag.String(f.Name, "", "")
153+
}
154+
155+
if f.Type == "bool" {
156+
parsed[f.Name] = flag.Bool(f.Name, false, "")
157+
}
158+
}
159+
for _, child := range c.Children {
160+
child.init(parsed)
161+
}
162+
}
163+
164+
type Example struct {
165+
Content string `json:"content"`
166+
Description Description `json:"description"`
167+
}
168+
169+
type Argument struct {
170+
Name string `json:"name"`
171+
Required bool `json:"required"`
172+
Description Description `json:"description"`
173+
}
174+
175+
type Description struct {
176+
Short string `json:"short,omitempty"`
177+
Long string `json:"long,omitempty"`
178+
}
179+
180+
type ArgumentList []Argument
181+
182+
func (a ArgumentList) String() string {
183+
args := []string{}
184+
for _, arg := range a {
185+
if arg.Required {
186+
args = append(args, "<"+arg.Name+">")
187+
} else {
188+
args = append(args, "["+arg.Name+"]")
189+
}
190+
}
191+
return strings.Join(args, " ")
192+
}
193+
194+
type Flag struct {
195+
Name string `json:"name"`
196+
Type string `json:"type"`
197+
Description Description `json:"description"`
198+
}
199+
200+
type CommandPath []Command
201+
202+
var ErrHelp = util.NewReadableError(nil, "")
203+
204+
func (c CommandPath) PrintHelp() error {
205+
prefix := []string{}
206+
for _, cmd := range c {
207+
prefix = append(prefix, cmd.Name)
208+
}
209+
active := c[len(c)-1]
210+
211+
if len(active.Children) > 0 {
212+
fmt.Print(strings.Join(prefix, " ") + ": ")
213+
fmt.Println(color.WhiteString(c[len(c)-1].Description.Short))
214+
215+
maxSubcommand := 0
216+
for _, child := range active.Children {
217+
if child.Hidden {
218+
continue
219+
}
220+
next := len(child.Name)
221+
if len(child.Args) > 0 {
222+
next += len(child.Args.String()) + 1
223+
}
224+
if next > maxSubcommand {
225+
maxSubcommand = next
226+
}
227+
}
228+
229+
fmt.Println()
230+
for _, child := range active.Children {
231+
if child.Hidden {
232+
continue
233+
}
234+
fmt.Printf(
235+
" %s %s %s\n",
236+
strings.Join(prefix, " "),
237+
color.New(color.FgWhite, color.Bold).Sprintf("%-*s", maxSubcommand, func() string {
238+
if len(child.Args) > 0 {
239+
return strings.Join([]string{child.Name, child.Args.String()}, " ")
240+
}
241+
return child.Name
242+
}()),
243+
child.Description.Short,
244+
)
245+
}
246+
}
247+
248+
if len(active.Children) == 0 {
249+
color.New(color.FgWhite, color.Bold).Print("Usage: ")
250+
color.New(color.FgCyan).Print(strings.Join(prefix, " "))
251+
if len(active.Args) > 0 {
252+
color.New(color.FgGreen).Print(" " + active.Args.String())
253+
}
254+
fmt.Println()
255+
fmt.Println()
256+
257+
color.New(color.FgWhite, color.Bold).Print("Flags:\n")
258+
maxFlag := 0
259+
for _, cmd := range c {
260+
for _, f := range cmd.Flags {
261+
l := len(f.Name) + 3
262+
if l > maxFlag {
263+
maxFlag = l
264+
}
265+
}
266+
}
267+
268+
for _, cmd := range c {
269+
for _, f := range cmd.Flags {
270+
fmt.Printf(
271+
" %s %s\n",
272+
color.New(color.FgMagenta).Sprintf("--%-*s", maxFlag, f.Name),
273+
f.Description.Short,
274+
)
275+
}
276+
}
277+
278+
if len(active.Examples) > 0 {
279+
fmt.Println()
280+
color.New(color.FgWhite, color.Bold).Print("Examples:\n")
281+
for _, example := range active.Examples {
282+
fmt.Println(" " + example.Content)
283+
}
284+
}
285+
}
286+
287+
fmt.Println()
288+
fmt.Printf("Learn more at %s\n", color.MagentaString("https://ion.sst.dev"))
289+
290+
return ErrHelp
291+
}
292+
293+
func (c *Cli) Stage(cfgPath string) (string, error) {
294+
stage := c.String("stage")
295+
if stage == "" {
296+
stage = os.Getenv("SST_STAGE")
297+
if stage == "" {
298+
stage = project.LoadPersonalStage(cfgPath)
299+
if stage == "" {
300+
stage = guessStage()
301+
if stage == "" {
302+
err := huh.NewForm(
303+
huh.NewGroup(
304+
huh.NewInput().Title(" Enter name for your personal stage").Prompt(" > ").Value(&stage).Validate(func(v string) error {
305+
if project.InvalidStageRegex.MatchString(v) {
306+
return fmt.Errorf("Invalid stage name")
307+
}
308+
return nil
309+
}),
310+
),
311+
).WithTheme(huh.ThemeCatppuccin()).Run()
312+
if err != nil {
313+
return "", err
314+
}
315+
}
316+
err := project.SetPersonalStage(cfgPath, stage)
317+
if err != nil {
318+
return "", err
319+
}
320+
}
321+
}
322+
}
323+
godotenv.Load(fmt.Sprintf(".env.%s", stage))
324+
return stage, nil
325+
}
326+
327+
func guessStage() string {
328+
u, err := user.Current()
329+
if err != nil {
330+
return ""
331+
}
332+
stage := strings.ToLower(u.Username)
333+
stage = project.InvalidStageRegex.ReplaceAllString(stage, "")
334+
335+
if stage == "root" || stage == "admin" || stage == "prod" || stage == "dev" || stage == "production" {
336+
return ""
337+
}
338+
return stage
339+
}

0 commit comments

Comments
 (0)