// svgc builds SVG images from .svgd text descriptions — vector // graphics an agent can author, verify (terminal preview + info) and // share to agent-helm with `helmd share out.svg`. package main import ( "flag" "fmt" "os" "path/filepath" "strings" "gitea.brasse-pc.eu/brasse/agent-tools/svg-maker/svg" ) var version = "dev" const usage = `svgc - SVG maker for agents Usage: svgc build [-o out.svg] [--preview] [--width N] svgc preview [--width N] draw it in the terminal svgc info counts, colors, bbox, warnings svgc example print an annotated example file svgc version The .svgd format ('#' comments, one element per line): canvas 200 120 required: width height bg #1a2029 optional background def accent #4f9cf9 named color, use as $accent rect 10 10 60 40 fill=$accent rx=6 circle 120 40 20 fill=#3fca7c stroke=white stroke-width=2 ellipse 60 90 30 12 fill=gray line 10 100 190 100 stroke=red width=3 polyline 10,20 30,40 50,10 stroke=white polygon 20,80 40,60 60,80 fill=#e0a63f path M10,10 L50,50 Q70,20 90,50 Z stroke=white text 100 60 "Hello agent-helm" size=14 fill=white anchor=middle bold group stroke=gray stroke-width=1 group attrs are inherited line 0 0 10 10 end Attributes: fill stroke stroke-width|width opacity fill-opacity stroke-opacity rx ry anchor size font dash linecap linejoin transform id, plus flags bold italic. Colors: #rgb #rrggbb, CSS names, none. Show the result in agent-helm: helmd share out.svg --note "diagram" ` const exampleFile = `# gauge.svgd - example: a small status card canvas 240 120 bg #12161f def ok #3fca7c def frame #262d3a rect 8 8 224 104 fill=none stroke=$frame stroke-width=2 rx=10 circle 40 60 22 fill=none stroke=$ok stroke-width=6 path M30,60 L38,68 L52,50 stroke=$ok width=5 linecap=round fill=none text 76 54 "backups" size=13 fill=#8a93a5 text 76 76 "all green" size=17 fill=white bold ` func main() { if len(os.Args) < 2 { fmt.Print(usage) os.Exit(2) } switch os.Args[1] { case "build": cmdBuild(os.Args[2:]) case "preview": cmdPreview(os.Args[2:]) case "info": cmdInfo(os.Args[2:]) case "example": fmt.Print(exampleFile) case "version", "--version", "-v": fmt.Println("svgc", version) case "help", "--help", "-h": fmt.Print(usage) default: die("unknown command %q — run 'svgc help'", os.Args[1]) } } func cmdBuild(args []string) { fs := flag.NewFlagSet("build", flag.ExitOnError) out := fs.String("o", "", "output file (default: input with .svg)") preview := fs.Bool("preview", false, "also draw the result in the terminal") width := fs.Int("width", 72, "preview width in characters") pos := parseInterspersed(fs, args) if len(pos) != 1 { die("build takes exactly one .svgd file") } xml, doc := load(pos[0]) dst := *out if dst == "" { dst = strings.TrimSuffix(pos[0], filepath.Ext(pos[0])) + ".svg" } if err := os.WriteFile(dst, []byte(xml), 0o644); err != nil { die("%v", err) } fmt.Printf("wrote %s (%d bytes)\n", dst, len(xml)) fmt.Print(svg.Info(doc)) if *preview { fmt.Print(svg.RenderGrid(doc, *width).ANSI()) } } func cmdPreview(args []string) { fs := flag.NewFlagSet("preview", flag.ExitOnError) width := fs.Int("width", 72, "preview width in characters") pos := parseInterspersed(fs, args) if len(pos) != 1 { die("preview takes exactly one .svgd file") } _, doc := load(pos[0]) fmt.Print(svg.RenderGrid(doc, *width).ANSI()) } func cmdInfo(args []string) { if len(args) != 1 { die("info takes exactly one .svgd file") } _, doc := load(args[0]) fmt.Print(svg.Info(doc)) } // load reads and builds a .svgd file (attrs normalized), dying with // the parser's line-numbered error on failure. Returns the SVG XML // and the parsed document. func load(path string) (string, *svg.Doc) { data, err := os.ReadFile(path) if err != nil { die("%v", err) } xml, doc, err := svg.Build(string(data)) if err != nil { die("%s: %v", path, err) } return xml, doc } // parseInterspersed lets flags appear before or after positional args. func parseInterspersed(fs *flag.FlagSet, args []string) []string { var flags, pos []string for i := 0; i < len(args); i++ { a := args[i] if len(a) > 1 && a[0] == '-' { flags = append(flags, a) name := strings.TrimLeft(a, "-") if eq := strings.Index(name, "="); eq >= 0 { continue } if f := fs.Lookup(name); f != nil { if bf, ok := f.Value.(interface{ IsBoolFlag() bool }); ok && bf.IsBoolFlag() { continue } } if i+1 < len(args) { i++ flags = append(flags, args[i]) } } else { pos = append(pos, a) } } fs.Parse(flags) return pos } func die(format string, args ...interface{}) { fmt.Fprintf(os.Stderr, "svgc: "+format+"\n", args...) os.Exit(1) }