Monorepo for Tangled tangled.org
2

Configure Feed

Select the types of activity you want to include in your feed.

cmd/dolly: support logotype generation and width-only sizes

- new -kind flag selects between the logo and the full logotype
- -template now accepts a directory of templates
- -size accepts a bare width; height is derived from the template
viewBox aspect ratio

Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.org>

author
Anirudh Oppiliappan
committer
Tangled
date (Jun 10, 2026, 5:28 PM +0300) commit 17d8efc0 parent a5d661f5 change-id vyrtttzv
+91 -21
+3 -3
.tangled/workflows/deploy-blog.yml
··· 21 21 22 22 - name: generate favicons 23 23 command: | 24 - ./dolly.out -template appview/pages/templates/fragments/dolly/logo.html -output appview/pages/static/logos/dolly.png -size 180x180 25 - ./dolly.out -template appview/pages/templates/fragments/dolly/logo.html -output appview/pages/static/logos/dolly.ico -size 48x48 26 - ./dolly.out -template appview/pages/templates/fragments/dolly/logo.html -output appview/pages/static/logos/dolly.svg -color currentColor -favicon 24 + ./dolly.out -template appview/pages/templates/fragments/dolly -output appview/pages/static/logos/dolly.png -size 180 25 + ./dolly.out -template appview/pages/templates/fragments/dolly -output appview/pages/static/logos/dolly.ico -size 48 26 + ./dolly.out -template appview/pages/templates/fragments/dolly -output appview/pages/static/logos/dolly.svg -color currentColor -favicon 27 27 28 28 - name: generate css 29 29 command: |
+13
appview/pages/templates/fragments/dolly/logotype.html
··· 1 1 {{ define "fragments/dolly/logotype" }} 2 2 <svg class="{{ .Classes }}" viewBox="0 0 118 31" fill="{{ or .FillColor "currentColor" }}" xmlns="http://www.w3.org/2000/svg"> 3 + {{ if .Favicon }} 4 + <style> 5 + :root { 6 + color: #000000; 7 + } 8 + 9 + @media (prefers-color-scheme: dark) { 10 + :root { 11 + color: #ffffff; 12 + } 13 + } 14 + </style> 15 + {{ end }} 3 16 <path fill-rule="evenodd" clip-rule="evenodd" d="M78.6118 9.75728C79.2832 9.75733 79.8454 9.87208 80.2983 10.101C80.7513 10.3249 81.1161 10.6064 81.3921 10.9448C81.6732 11.278 81.8895 11.6064 82.0405 11.9292H82.1655V9.91353H85.4702V22.0307C85.4702 23.0305 85.2201 23.8668 84.7202 24.5385C84.2203 25.2154 83.5273 25.7251 82.6421 26.0688C81.762 26.4125 80.7488 26.5844 79.603 26.5844C78.5252 26.5844 77.6012 26.4385 76.8306 26.1469C76.065 25.8553 75.4555 25.462 75.0024 24.9672C74.5495 24.4726 74.2551 23.9237 74.1196 23.3198L77.1978 22.9057C77.2915 23.1243 77.4401 23.3303 77.6431 23.5229C77.8461 23.7206 78.1147 23.8824 78.4478 24.0073C78.7861 24.1321 79.1973 24.1947 79.6812 24.1948C80.4049 24.1948 81.0016 24.0227 81.4702 23.6792C81.9441 23.3355 82.1811 22.7701 82.1812 21.9838V19.7885H82.0405C81.8947 20.1217 81.6758 20.4371 81.3843 20.7338C81.0927 21.0306 80.7174 21.273 80.2593 21.4604C79.8012 21.6477 79.2546 21.7416 78.6196 21.7417C77.7188 21.7417 76.8981 21.5332 76.1587 21.1167C75.4244 20.6948 74.8383 20.0514 74.4009 19.187C73.9686 18.3172 73.7524 17.2181 73.7524 15.8901C73.7524 14.5308 73.9738 13.3952 74.4165 12.4838C74.8592 11.5726 75.4479 10.89 76.1821 10.437C76.9216 9.98403 77.7318 9.75728 78.6118 9.75728ZM79.6733 12.4057C79.1269 12.4058 78.6663 12.5544 78.2915 12.851C77.9166 13.1426 77.6326 13.5491 77.4399 14.0698C77.2472 14.5906 77.1509 15.1922 77.1509 15.8745C77.1509 16.5671 77.2472 17.1662 77.4399 17.6713C77.6378 18.1712 77.9218 18.5594 78.2915 18.8354C78.6663 19.106 79.1269 19.2416 79.6733 19.2417C80.2096 19.2417 80.663 19.1087 81.0327 18.8432C81.4076 18.5724 81.6942 18.1868 81.8921 17.687C82.0952 17.1818 82.1968 16.5776 82.1968 15.8745C82.1968 15.1714 82.0978 14.5619 81.8999 14.0463C81.702 13.5256 81.4155 13.1218 81.0405 12.8354C80.6656 12.5491 80.2096 12.4057 79.6733 12.4057Z" fill="{{ or .FillColor "currentColor" }}"/> 4 17 <path fill-rule="evenodd" clip-rule="evenodd" d="M98.7397 9.75728C99.531 9.75733 100.268 9.88506 100.95 10.1401C101.637 10.39 102.236 10.7679 102.747 11.2729C103.262 11.778 103.663 12.4137 103.95 13.1792C104.236 13.9395 104.379 14.8303 104.379 15.851V16.7651H96.2085V16.7729C96.2085 17.3666 96.3179 17.8797 96.5366 18.312C96.7606 18.7441 97.0758 19.0776 97.4819 19.312C97.888 19.5461 98.3694 19.6635 98.9263 19.6635C99.2959 19.6635 99.6347 19.6113 99.9419 19.5073C100.249 19.4032 100.512 19.2467 100.731 19.0385C100.95 18.8303 101.116 18.5749 101.231 18.2729L104.309 18.476C104.153 19.2155 103.832 19.8615 103.348 20.4135C102.869 20.9603 102.249 21.3876 101.489 21.6948C100.734 21.9967 99.8609 22.1479 98.8716 22.1479C97.6376 22.1478 96.5754 21.8977 95.6851 21.3979C94.7998 20.8928 94.1173 20.179 93.6382 19.2573C93.159 18.3302 92.9194 17.2338 92.9194 15.9682C92.9194 14.7339 93.159 13.6505 93.6382 12.7182C94.1173 11.7861 94.792 11.0593 95.6616 10.5385C96.5365 10.0179 97.5629 9.75728 98.7397 9.75728ZM98.7935 12.2417C98.2886 12.2417 97.8411 12.3591 97.4507 12.5932C97.0654 12.8223 96.7632 13.1324 96.5444 13.5229C96.3427 13.8784 96.2325 14.2718 96.2124 14.7026H101.247C101.247 14.2235 101.142 13.7989 100.934 13.4292C100.726 13.0595 100.437 12.7702 100.067 12.562C99.7024 12.3486 99.2776 12.2417 98.7935 12.2417Z" fill="{{ or .FillColor "currentColor" }}"/> 5 18 <path fill-rule="evenodd" clip-rule="evenodd" d="M54.187 9.75728C54.8536 9.7573 55.4918 9.83543 56.1011 9.99165C56.7155 10.1479 57.26 10.3902 57.7339 10.7182C58.2128 11.0462 58.5897 11.4685 58.8657 11.9838C59.1417 12.4942 59.2798 13.1063 59.2798 13.8198V21.9135H56.1245V20.2495H56.0308C55.8381 20.6244 55.5802 20.9552 55.2573 21.2417C54.9345 21.5229 54.5463 21.7443 54.0933 21.9057C53.6402 22.0619 53.1166 22.1401 52.5229 22.1401C51.7574 22.1401 51.075 22.0072 50.4761 21.7417C49.8772 21.4709 49.4031 21.0723 49.0542 20.5463C48.7105 20.0152 48.5386 19.3535 48.5386 18.562C48.5386 17.8954 48.661 17.3354 48.9058 16.8823C49.1505 16.4292 49.4839 16.0646 49.9058 15.7885C50.3276 15.5125 50.8068 15.3041 51.3433 15.1635C51.8849 15.0229 52.4527 14.9239 53.0464 14.8667C53.7442 14.7937 54.3069 14.726 54.7339 14.6635C55.1608 14.5958 55.4709 14.4968 55.6636 14.3667C55.8561 14.2365 55.9526 14.0436 55.9526 13.7885V13.7417C55.9526 13.247 55.7963 12.864 55.4839 12.5932C55.1767 12.3224 54.7389 12.187 54.1714 12.187C53.5726 12.187 53.0958 12.3199 52.7417 12.5854C52.3876 12.8458 52.1532 13.174 52.0386 13.5698L48.9604 13.3198C49.1167 12.5907 49.424 11.9603 49.8823 11.4292C50.3406 10.8928 50.9319 10.4812 51.6558 10.1948C52.3848 9.90318 53.2288 9.75728 54.187 9.75728ZM55.9761 16.351C55.872 16.4187 55.7285 16.4813 55.5464 16.5385C55.3694 16.5906 55.1687 16.6401 54.9448 16.687C54.721 16.7286 54.4968 16.7677 54.2729 16.8042C54.0491 16.8354 53.8458 16.8641 53.6636 16.8901C53.273 16.9474 52.9318 17.0386 52.6401 17.1635C52.3485 17.2885 52.1219 17.4578 51.9604 17.6713C51.799 17.8797 51.7183 18.1401 51.7183 18.4526C51.7183 18.9056 51.8824 19.2521 52.2104 19.4917C52.5437 19.7259 52.9658 19.8432 53.4761 19.8432C53.9655 19.8432 54.398 19.7468 54.7729 19.5542C55.1479 19.3563 55.4423 19.0906 55.6558 18.7573C55.8693 18.424 55.9761 18.0463 55.9761 17.6245V16.351Z" fill="{{ or .FillColor "currentColor" }}"/>
+66 -10
cmd/dolly/main.go
··· 8 8 "image" 9 9 "image/color" 10 10 "image/png" 11 + "math" 11 12 "os" 12 13 "path/filepath" 13 14 "strconv" ··· 26 27 fillColor string 27 28 output string 28 29 templatePath string 30 + kind string 29 31 favicon bool 30 32 ) 31 33 32 - flag.StringVar(&templatePath, "template", "", "Path to dolly go-html template") 33 - flag.StringVar(&size, "size", "512x512", "Output size in format WIDTHxHEIGHT (e.g., 512x512)") 34 + flag.StringVar(&templatePath, "template", "", "Path to a dolly go-html template file, or a directory of templates") 35 + flag.StringVar(&size, "size", "512", "Output size as WIDTH (height derived from aspect ratio, e.g., 512) or WIDTHxHEIGHT (e.g., 512x512)") 34 36 flag.StringVar(&fillColor, "color", "#000000", "Fill color in hex format (e.g., #FF5733)") 35 37 flag.StringVar(&output, "output", "dolly.svg", "Output file path (format detected from extension: .svg, .png, or .ico)") 38 + flag.StringVar(&kind, "kind", "logo", "Asset to generate: logo (dolly only) or logotype (dolly + wordmark)") 36 39 flag.BoolVar(&favicon, "favicon", false, "Embed a prefers-color-scheme style block so the SVG reacts to dark mode (SVG output only)") 37 40 flag.Parse() 38 41 ··· 41 44 os.Exit(1) 42 45 } 43 46 47 + if kind != "logo" && kind != "logotype" { 48 + fmt.Fprintf(os.Stderr, "Invalid kind: %s. Must be logo or logotype\n", kind) 49 + os.Exit(1) 50 + } 51 + 44 52 width, height, err := parseSize(size) 45 53 if err != nil { 46 54 fmt.Fprintf(os.Stderr, "Error parsing size: %v\n", err) ··· 61 69 os.Exit(1) 62 70 } 63 71 64 - tpl, err := os.ReadFile(templatePath) 72 + tpl, err := loadTemplates(templatePath) 65 73 if err != nil { 66 - fmt.Fprintf(os.Stderr, "Failed to read template from path %s: %v\n", templatePath, err) 74 + fmt.Fprintf(os.Stderr, "Failed to load templates from path %s: %v\n", templatePath, err) 67 75 os.Exit(1) 68 76 } 69 77 ··· 72 80 os.Exit(1) 73 81 } 74 82 75 - svgData, err := dolly(string(tpl), fillColor, favicon) 83 + svgData, err := dolly(tpl, "fragments/dolly/"+kind, fillColor, favicon) 76 84 if err != nil { 77 85 fmt.Fprintf(os.Stderr, "Error generating SVG: %v\n", err) 78 86 os.Exit(1) 79 87 } 80 88 89 + // Derive height from the SVG's aspect ratio when only a width was given 90 + if height == 0 && format != "svg" { 91 + height, err = deriveHeight(svgData, width) 92 + if err != nil { 93 + fmt.Fprintf(os.Stderr, "Error deriving height: %v\n", err) 94 + os.Exit(1) 95 + } 96 + } 97 + 81 98 // Create output directory if it doesn't exist 82 99 dir := filepath.Dir(output) 83 100 if dir != "" && dir != "." { ··· 101 118 os.Exit(1) 102 119 } 103 120 104 - fmt.Printf("Successfully generated %s (%dx%d)\n", output, width, height) 121 + if format == "svg" { 122 + // size is irrelevant for svg output; it scales to its viewBox 123 + fmt.Printf("Successfully generated %s\n", output) 124 + } else { 125 + fmt.Printf("Successfully generated %s (%dx%d)\n", output, width, height) 126 + } 105 127 } 106 128 107 - func dolly(tplString, hexColor string, favicon bool) ([]byte, error) { 108 - tpl, err := template.New("dolly").Parse(tplString) 129 + func loadTemplates(path string) (*template.Template, error) { 130 + info, err := os.Stat(path) 109 131 if err != nil { 110 132 return nil, err 111 133 } 112 134 135 + if info.IsDir() { 136 + return template.ParseGlob(filepath.Join(path, "*.html")) 137 + } 138 + 139 + return template.ParseFiles(path) 140 + } 141 + 142 + func dolly(tpl *template.Template, name, hexColor string, favicon bool) ([]byte, error) { 113 143 var svgData bytes.Buffer 114 - if err := tpl.ExecuteTemplate(&svgData, "fragments/dolly/logo", map[string]any{ 144 + if err := tpl.ExecuteTemplate(&svgData, name, map[string]any{ 115 145 "FillColor": hexColor, 116 146 "Classes": "", 117 147 "Favicon": favicon, ··· 138 168 return rgba, nil 139 169 } 140 170 171 + // parseSize parses WIDTH or WIDTHxHEIGHT. A height of 0 means "derive 172 + // from the SVG's aspect ratio". 141 173 func parseSize(size string) (int, int, error) { 174 + if !strings.Contains(size, "x") { 175 + width, err := strconv.Atoi(size) 176 + if err != nil { 177 + return 0, 0, fmt.Errorf("invalid width: %v", err) 178 + } 179 + if width <= 0 { 180 + return 0, 0, fmt.Errorf("width must be positive") 181 + } 182 + return width, 0, nil 183 + } 184 + 142 185 parts := strings.Split(size, "x") 143 186 if len(parts) != 2 { 144 - return 0, 0, fmt.Errorf("invalid size format, use WIDTHxHEIGHT") 187 + return 0, 0, fmt.Errorf("invalid size format, use WIDTH or WIDTHxHEIGHT") 145 188 } 146 189 147 190 width, err := strconv.Atoi(parts[0]) ··· 159 202 } 160 203 161 204 return width, height, nil 205 + } 206 + 207 + func deriveHeight(svgData []byte, width int) (int, error) { 208 + icon, err := oksvg.ReadIconStream(bytes.NewReader(svgData)) 209 + if err != nil { 210 + return 0, fmt.Errorf("error parsing SVG: %v", err) 211 + } 212 + 213 + if icon.ViewBox.W <= 0 || icon.ViewBox.H <= 0 { 214 + return 0, fmt.Errorf("SVG has an invalid viewBox (%gx%g)", icon.ViewBox.W, icon.ViewBox.H) 215 + } 216 + 217 + return int(math.Round(float64(width) * icon.ViewBox.H / icon.ViewBox.W)), nil 162 218 } 163 219 164 220 func isValidHexColor(hex string) bool {
+3 -3
localinfra/scripts/appview-static-files.sh
··· 38 38 cp -f "$TMP/actor-typeahead/actor-typeahead.js" "$OUT/" 39 39 40 40 (cd "$REPO_ROOT" && go build -o "$TMP/dolly" ./cmd/dolly) 41 - TEMPLATE="$REPO_ROOT/appview/pages/templates/fragments/dolly/logo.html" 42 - "$TMP/dolly" -template "$TEMPLATE" -output "$OUT/logos/dolly.png" -size 180x180 43 - "$TMP/dolly" -template "$TEMPLATE" -output "$OUT/logos/dolly.ico" -size 48x48 41 + TEMPLATE="$REPO_ROOT/appview/pages/templates/fragments/dolly" 42 + "$TMP/dolly" -template "$TEMPLATE" -output "$OUT/logos/dolly.png" -size 180 43 + "$TMP/dolly" -template "$TEMPLATE" -output "$OUT/logos/dolly.ico" -size 48 44 44 "$TMP/dolly" -template "$TEMPLATE" -output "$OUT/logos/dolly.svg" -color currentColor -favicon
+2 -2
nix/pkgs/appview-static-files.nix
··· 30 30 cp -f ${ibm-plex-mono-src}/fonts/complete/woff2/IBMPlexMono*.woff2 fonts/ 31 31 cp -f ${actor-typeahead-src}/actor-typeahead.js . 32 32 33 - ${dolly}/bin/dolly -output logos/dolly.png -size 180x180 34 - ${dolly}/bin/dolly -output logos/dolly.ico -size 48x48 33 + ${dolly}/bin/dolly -output logos/dolly.png -size 180 34 + ${dolly}/bin/dolly -output logos/dolly.ico -size 48 35 35 ${dolly}/bin/dolly -output logos/dolly.svg -color currentColor -favicon 36 36 # tailwindcss -c $src/tailwind.config.js -i $src/input.css -o tw.css won't work 37 37 # for whatever reason (produces broken css), so we are doing this instead
+2 -2
nix/pkgs/docs.nix
··· 54 54 cp -f ${ibm-plex-mono-src}/fonts/complete/woff2/IBMPlexMono*.woff2 $out/static/fonts/ 55 55 56 56 # favicons 57 - ${dolly}/bin/dolly -output $out/static/logos/dolly.png -size 180x180 58 - ${dolly}/bin/dolly -output $out/static/logos/dolly.ico -size 48x48 57 + ${dolly}/bin/dolly -output $out/static/logos/dolly.png -size 180 58 + ${dolly}/bin/dolly -output $out/static/logos/dolly.ico -size 48 59 59 ${dolly}/bin/dolly -output $out/static/logos/dolly.svg -color currentColor -favicon 60 60 61 61 # styles
+2 -1
nix/pkgs/dolly.nix
··· 11 11 ../../ico 12 12 ../../cmd/dolly/main.go 13 13 ../../appview/pages/templates/fragments/dolly/logo.html 14 + ../../appview/pages/templates/fragments/dolly/logotype.html 14 15 ]; 15 16 }; 16 17 dolly-unwrapped = buildGoApplication { ··· 23 24 in 24 25 writeShellScriptBin "dolly" '' 25 26 exec ${dolly-unwrapped}/bin/dolly \ 26 - -template ${src}/appview/pages/templates/fragments/dolly/logo.html \ 27 + -template ${src}/appview/pages/templates/fragments/dolly \ 27 28 "$@" 28 29 ''