1 package d2cli
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "os"
8 "os/exec"
9 "path/filepath"
10 "strings"
11
12 "oss.terrastruct.com/util-go/xmain"
13
14 "oss.terrastruct.com/d2/d2plugin"
15 "oss.terrastruct.com/d2/d2themes/d2themescatalog"
16 "oss.terrastruct.com/d2/lib/version"
17 )
18
19 func help(ms *xmain.State) {
20 fmt.Fprintf(ms.Stdout, `%[1]s %[2]s
21 Usage:
22 %[1]s [--watch=false] [--theme=0] file.d2 [file.svg | file.png]
23 %[1]s layout [name]
24 %[1]s fmt file.d2 ...
25
26 %[1]s compiles and renders file.d2 to file.svg | file.png
27 It defaults to file.svg if an output path is not provided.
28
29 Use - to have d2 read from stdin or write to stdout.
30
31 See man d2 for more detailed docs.
32
33 Flags:
34 %[3]s
35
36 Subcommands:
37 %[1]s layout - Lists available layout engine options with short help
38 %[1]s layout [name] - Display long help for a particular layout engine, including its configuration options
39 %[1]s themes - Lists available themes
40 %[1]s fmt file.d2 ... - Format passed files
41
42 See more docs and the source code at https://oss.terrastruct.com/d2.
43 Hosted icons at https://icons.terrastruct.com.
44 Playground runner at https://play.d2lang.com.
45 `, filepath.Base(ms.Name), version.Version, ms.Opts.Defaults())
46 }
47
48 func layoutCmd(ctx context.Context, ms *xmain.State, ps []d2plugin.Plugin) error {
49 if len(ms.Opts.Flags.Args()) == 1 {
50 return shortLayoutHelp(ctx, ms, ps)
51 } else if len(ms.Opts.Flags.Args()) == 2 {
52 return longLayoutHelp(ctx, ms, ps)
53 } else {
54 return pluginSubcommand(ctx, ms, ps)
55 }
56 }
57
58 func themesCmd(ctx context.Context, ms *xmain.State) {
59 fmt.Fprintf(ms.Stdout, "Available themes:\n%s", d2themescatalog.CLIString())
60 }
61
62 func shortLayoutHelp(ctx context.Context, ms *xmain.State, ps []d2plugin.Plugin) error {
63 var pluginLines []string
64 pinfos, err := d2plugin.ListPluginInfos(ctx, ps)
65 if err != nil {
66 return err
67 }
68 for _, p := range pinfos {
69 var l string
70 if p.Type == "bundled" {
71 l = fmt.Sprintf("%s (bundled) - %s", p.Name, p.ShortHelp)
72 } else {
73 l = fmt.Sprintf("%s (%s) - %s", p.Name, humanPath(p.Path), p.ShortHelp)
74 }
75 pluginLines = append(pluginLines, l)
76 }
77 fmt.Fprintf(ms.Stdout, `Available layout engines found:
78
79 %s
80
81 Usage:
82 To use a particular layout engine, set the environment variable D2_LAYOUT=[name] or flag --layout=[name].
83
84 Example:
85 D2_LAYOUT=dagre d2 in.d2 out.svg
86
87 Subcommands:
88 %s layout [layout name] - Display long help for a particular layout engine, including its configuration options
89
90 See more docs at https://d2lang.com/tour/layouts
91 `, strings.Join(pluginLines, "\n"), ms.Name)
92 return nil
93 }
94
95 func longLayoutHelp(ctx context.Context, ms *xmain.State, ps []d2plugin.Plugin) error {
96 layout := ms.Opts.Flags.Arg(1)
97 plugin, err := d2plugin.FindPlugin(ctx, ps, layout)
98 if err != nil {
99 if errors.Is(err, exec.ErrNotFound) {
100 return layoutNotFound(ctx, ps, layout)
101 }
102 return err
103 }
104
105 pinfo, err := plugin.Info(ctx)
106 if err != nil {
107 return err
108 }
109
110 plocation := pinfo.Type
111 if pinfo.Type == "binary" {
112 plocation = fmt.Sprintf("executable plugin at %s", humanPath(pinfo.Path))
113 }
114
115 if !strings.HasSuffix(pinfo.LongHelp, "\n") {
116 pinfo.LongHelp += "\n"
117 }
118 fmt.Fprintf(ms.Stdout, `%s (%s):
119
120 %s`, pinfo.Name, plocation, pinfo.LongHelp)
121
122 return nil
123 }
124
125 func layoutNotFound(ctx context.Context, ps []d2plugin.Plugin, layout string) error {
126 pinfos, err := d2plugin.ListPluginInfos(ctx, ps)
127 if err != nil {
128 return err
129 }
130 var names []string
131 for _, p := range pinfos {
132 names = append(names, p.Name)
133 }
134
135 return xmain.UsageErrorf(`D2_LAYOUT "%s" is not bundled and could not be found in your $PATH.
136 The available options are: %s. For details on each option, run "d2 layout".
137
138 For more information on setup, please visit https://github.com/terrastruct/d2.`,
139 layout, strings.Join(names, ", "))
140 }
141
142 func pluginSubcommand(ctx context.Context, ms *xmain.State, ps []d2plugin.Plugin) error {
143 layout := ms.Opts.Flags.Arg(1)
144 plugin, err := d2plugin.FindPlugin(ctx, ps, layout)
145 if err != nil {
146 if errors.Is(err, exec.ErrNotFound) {
147 return layoutNotFound(ctx, ps, layout)
148 }
149 return err
150 }
151
152 ms.Opts.Args = ms.Opts.Flags.Args()[2:]
153 return d2plugin.Serve(plugin)(ctx, ms)
154 }
155
156 func humanPath(fp string) string {
157 if strings.HasPrefix(fp, os.Getenv("HOME")) {
158 return filepath.Join("~", strings.TrimPrefix(fp, os.Getenv("HOME")))
159 }
160 return fp
161 }
162
View as plain text