svg-maker: svgc - .svgd text format -> SVG for agent-helm sharing
Line-based DSL (shapes, text, groups, color vars) with strict validation and line-numbered errors, SVG emitter with visibility defaults, truecolor terminal preview (same half-block technique as spritec), info with bbox + outside-canvas warnings. Verified end-to-end: svgc build -> helmd share -> visible through the hub. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011KikHkfCiC3yELbsMN8fT9
This commit is contained in:
176
svg-maker/main.go
Normal file
176
svg-maker/main.go
Normal file
@@ -0,0 +1,176 @@
|
||||
// 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 <file.svgd> [-o out.svg] [--preview] [--width N]
|
||||
svgc preview <file.svgd> [--width N] draw it in the terminal
|
||||
svgc info <file.svgd> 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)
|
||||
}
|
||||
Reference in New Issue
Block a user