// ppppcg is a PrePostPrint inspired political compass generator with SVG output. package main import ( "flag" "fmt" "log" "os" "path/filepath" svg "github.com/ajstarks/svgo" "gopkg.in/yaml.v2" ) // help is the CLI help text. const help = `ppppcg [options] A PrePostPrint inspired political compass generator with SVG output. Options: -h output help ` // config is the user configuration file. type config struct { Width int Height int Axis struct { Top string Right string Bottom string Left string } Points []struct { Name string X int Y int } } var ( helpFlag bool configFlag string // rectMargin is the margin for the compass rectangle. rectMargin = 50 // fillNoneStrokeBlack is a default calm line style. fillNoneStrokeBlack = "fill:none;stroke:black" // axisFontStyle is the default axis font style axisFontStyle = "font-size: 13pt;font-weight:bold" // pointFontStyle is the default point font style pointFontStyle = "font-size: 11pt;font-style:italic" // lineOverflow is the amount of overflow permitted for each compass line. lineOverflow = 10 ) // handleCliFlags parses CLI flags. func handleCliFlags() { flag.BoolVar(&helpFlag, "h", false, "output help") flag.StringVar(&configFlag, "c", "ppppcg.yml", "config file") flag.Parse() } // loadYAMLConfig loads a mppppcg YAML user config. func loadYAMLConfig() (config, error) { var cfg config configPath, err := filepath.Abs(configFlag) if err != nil { return config{}, fmt.Errorf("loadYAMLConfig: unable to convert %s to an absolute path: %w", configFlag, err) } conf, err := os.ReadFile(configPath) if err != nil { return config{}, fmt.Errorf("loadYAMLConfig: unable to read %s: %w", configFlag, err) } err = yaml.UnmarshalStrict(conf, &cfg) if err != nil { return config{}, fmt.Errorf("loadYAMLConfig: unable to unmarshal %s: %w", string(conf), err) } return cfg, nil } // main is the CLI entrypoint. func main() { handleCliFlags() if helpFlag { fmt.Print(help) os.Exit(0) } cfg, err := loadYAMLConfig() if err != nil { log.Fatal(err) } canvas := svg.New(os.Stdout) canvas.Start(cfg.Width, cfg.Height) canvas.CenterRect( cfg.Width/2, cfg.Height/2, cfg.Width-rectMargin, cfg.Width-rectMargin, fillNoneStrokeBlack, ) canvas.Line( cfg.Width/2, lineOverflow, cfg.Width/2, cfg.Width-lineOverflow, fillNoneStrokeBlack, ) canvas.Line( lineOverflow, cfg.Width/2, cfg.Width-lineOverflow, cfg.Width/2, fillNoneStrokeBlack, ) canvas.TranslateRotate( lineOverflow, cfg.Width/2, 270, ) canvas.Text( -15, 35, cfg.Axis.Left, axisFontStyle, ) canvas.Gend() canvas.Text( cfg.Width/2+10, lineOverflow+10, cfg.Axis.Top, axisFontStyle, ) canvas.TranslateRotate( cfg.Width-lineOverflow, (cfg.Width/2)-lineOverflow+5, 90, ) canvas.Text( 35, -(cfg.Width/2)-3, cfg.Axis.Right, axisFontStyle, ) canvas.Gend() canvas.Text( (cfg.Width/2)+lineOverflow, cfg.Width-lineOverflow, cfg.Axis.Bottom, axisFontStyle, ) for _, p := range cfg.Points { canvas.Text(p.X, p.Y, p.Name, pointFontStyle) } canvas.End() }